Go + App Engine =

Johan Euphrosine
October 5, 2013

proppy-go-ae.appspot.com

About Me

proppy-go-ae.appspot.com

App Engine

App Engine allows you to scale your web application on Google Infrastructure

proppy-go-ae.appspot.com

Go Runtime

proppy-go-ae.appspot.com

Hello Gopher!

package gopher

import (
    "fmt"
    "net/http"
)

// The init function is called when your application starts.
func init() {
    // Register a handle for /hello URLs.
    http.HandleFunc("/hello", hello)
}

// hello is an HTTP handler that prints "Hello Gopher!"
func hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Hello, Gopher!")
}
proppy-go-ae.appspot.com

Hello App Engine!

application: gopher
version: 1
runtime: go
api_version: go1

handlers:
- url: /images
  static_dir: images
- url: /doc
  static_dir: doc
- url: /.*
  script: _go_app
$ dev_appserver.py myapp/
$ appcfg.py update myapp/
proppy-go-ae.appspot.com

Demo

Hello Gopher!

proppy-go-ae.appspot.com

Hello hackernews!

package gopher

import (
    "appengine"
    "appengine/urlfetch"
    "encoding/xml"
    "fmt"
    "net/http"
    "time"
)

func init() {
    // Register a handler for /hackernews URL.
    http.HandleFunc("/hackernews", hackernews)
}
proppy-go-ae.appspot.com

Map XML to Go types

<rss version="2.0">
  <channel>
    <item>
      <title />
      <link />
...
type HNFeed struct {
    Data struct {
        Items []Item `xml:"item"`
    } `xml:"channel"`
}

type Item struct {
    Title string `xml:"title"`
    URL   string `xml:"link"`
}
proppy-go-ae.appspot.com

Decode

func hackernewsItems(c appengine.Context) []Item {
    client := urlfetch.Client(c)
    resp, err := client.Get(HNFeedURL)
    if err != nil {
        c.Errorf("Error fetching %s: %s", HNFeedURL, err)
        return nil
    }
    defer resp.Body.Close()
    decoder := xml.NewDecoder(resp.Body)
    var feed *HNFeed
    if err = decoder.Decode(&feed); err != nil {
        c.Errorf("Error decoding HNFeed: %s", err)
        return nil
    }
    return feed.Data.Items
}
proppy-go-ae.appspot.com

And Print

func hackernews(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    w.Header().Add("Content-Type", "text/plain; charset=utf-8")
    for _, item := range hackernewsItems(c) {
        fmt.Fprintf(w, "%s: %s\n", item.Title, item.URL)
    }
}
proppy-go-ae.appspot.com

Demo

Hello Hacker News!

proppy-go-ae.appspot.com

Hello proggit!

package gopher

import (
    "appengine"
    "appengine/urlfetch"
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

func init() {
    // Register a handler for /proggit URL.
    http.HandleFunc("/proggit", proggit)
}
proppy-go-ae.appspot.com

Map Json to Go types

{"data": { "children": [
   {"data": { "title": ... , "url": ...}}, ...
type RedditFeed struct {
    Data struct {
        Items [] struct {
            Data Item
        } `json:"children"`
    }
}
type Item struct {
    Title string `xml:"title"`
    URL   string `xml:"link"`
}
proppy-go-ae.appspot.com

Decode

func proggitItems(c appengine.Context) []Item {
    client := urlfetch.Client(c)
    resp, err := client.Get(ProggitFeedURL)
    if err != nil {
        c.Errorf("Error fetching %s: %s", ProggitFeedURL, err)
        return nil
    }
    defer resp.Body.Close()
    decoder := json.NewDecoder(resp.Body)
    var feed *RedditFeed
    if err = decoder.Decode(&feed); err != nil {
        c.Errorf("Error decoding RedditFeed: %s", err)
        return nil
    }
    items := make([]Item, len(feed.Data.Items))
    for i, item := range feed.Data.Items {
        items[i] = item.Data
    }
    return items
}
proppy-go-ae.appspot.com

And print

func proggit(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    w.Header().Add("Content-Type", "text/plain; charset=utf-8")
    for _, item := range proggitItems(c) {
        fmt.Fprintf(w, "%s: %s\n", item.Title, item.URL)
    }
}
proppy-go-ae.appspot.com

Demo

Hello Proggit!

proppy-go-ae.appspot.com

Progginator!

func progginator(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    hnItems := hackernewsItems(c)
    pgItems := proggitItems(c)
    w.Header().Add("Content-Type", "text/plain; charset=utf-8")
    for _, item := range hnItems {
        fmt.Fprintf(w, "%s: %s\n", item.Title, item.URL)
    }
    for _, item := range pgItems {
        fmt.Fprintf(w, "%s: %s\n", item.Title, item.URL)
    }
}
proppy-go-ae.appspot.com

Demo

Progginator! proppy-go-ae.appspot.com

2012-03-15 04:17:59.148 hackernews: 261.695ms
2012-03-15 04:17:59.246 proggit: 97.961ms
2012-03-15 04:17:59.246 progginator: 359.822ms

Make it concurrent

func progginator_(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    results := make(chan []Item)
    go func() {
        results <- hackernewsItems(c)
    }()
    go func() {
        results <- proggitItems(c)
    }()
    w.Header().Add("Content-Type", "text/plain; charset=utf-8")
    for i := 0; i < 2; i++ {
        items := <-results
        for _, item := range items {
            fmt.Fprintf(w, "%s: %s\n", item.Title, item.URL)
        }
    }
}
proppy-go-ae.appspot.com

Demo

Progginator!

2012-03-15 04:18:03.804 proggit: 110.451ms
2012-03-15 04:18:03.887 hackernews: 193.751ms
2012-03-15 04:18:03.887 progginator_: 193.824ms
proppy-go-ae.appspot.com

Fetch feed offline

cron:
- description: update feed
  url: /fetch
  schedule: every 5 minutes
proppy-go-ae.appspot.com

Store feed in Datastore

type Feed struct {
    Items []Item `datastore:",noindex"`
}
type Item struct {
    Title string `xml:"title"`
    URL   string `xml:"link"`
}
proppy-go-ae.appspot.com

Store feed in Datastore

func fetch(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    done := make(chan error)
    go func() {
        key := datastore.NewKey(c, "Feed", "hackernews", 0, nil)
        val := &Feed{Items: hackernewsItems(c)}
        _, err := datastore.Put(c, key, val)
        done <- err
    }()
    go func() {
        key := datastore.NewKey(c, "Feed", "proggit", 0, nil)
        val := &Feed{Items: proggitItems(c)}
        _, err := datastore.Put(c, key, val)
        done <- err
    }()
    // Wait for goroutines
    // ...
proppy-go-ae.appspot.com

Read feed from Datastore

func progginator__(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    feeds := make([]Feed, 2)
    keys := []*datastore.Key{
        datastore.NewKey(c, "Feed", "proggit", 0, nil),
        datastore.NewKey(c, "Feed", "hackernews", 0, nil),
    }
    err := datastore.GetMulti(c, keys, feeds)
    if err != nil {
        c.Errorf("error getting feed from datastore: %v", err)
        return
    }
    // Render feeds
    // ...
proppy-go-ae.appspot.com

Demo

Progginator2 w/ Datastore!

progginator__: 9.638258ms
proppy-go-ae.appspot.com

Cache feed in memcache

func progginator___(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    feeds := make([]Feed, 2)
    _, err := memcache.Gob.Get(c, "feeds", &feeds)
    if err != nil {
        // Get feeds from datastore
        // ...
        _ = memcache.Gob.Set(c, &memcache.Item{
            Key: "feeds",
            Object: feeds,
        })
    }
    // Render feeds
    // ...
proppy-go-ae.appspot.com

Demo

Progginator3 w/ Memcache!

progginator___: 2.740829ms
proppy-go-ae.appspot.com

Takeaways

proppy-go-ae.appspot.com

Homework

proppy-go-ae.appspot.com

Thank you!

	 := "developers.google.com/appengine/docs/go/"
	 := "golang.org"
	 := "profiles.google.com/proppy"
proppy-go-ae.appspot.com