GAE/Go ハンズオン

build simple application with GAE/Go

14 June 2014

Daigo Ikeda

Knightso, LLC

アジェンダ

GAE/Go紹介

GAEとは

GAE機能

GAE/Goの特徴

速い!

特にspin-upが。

もちろん実行速度も。

Built-in Concurrency!

平行処理をシンプルに書ける→パフォーマンスチューニングが容易
つまり課金が抑えられる!!

開発環境構築

Goのインストール

不要!
Google App Engine SDKにGoの環境が含まれている。

Google App Engine SDK for Goのインストール

自分のPlatform用SDKをダウンロードして任意の場所に展開して下さい。

環境設定

環境変数を設定。

Mac/Linux

$export GAEGO=<SDKをインストールしたパス>
$export GOROOT=$GAEGO/goroot
$export PATH=$GAEGO:$PATH

Windows(未確認)

>set GAEGO=<SDKをインストールしたパス>
>set GOROOT=%GAEGO%\goroot
>set PATH=%GAEGO%;%PATH%

毎回設定するのが面倒なら.bash_profileに記述するなり環境変数に設定するなり・・・ご自由に。

FYI:

Hello World作ってみよう!

ファイル構成

helloworld/
| hello/
| | hello.go
| app.yaml

cheat:

hello.go

package hello

import (
    "fmt"
    "net/http"
)

func init() {
    http.HandleFunc("/", handler)
}

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Hello, world!")
}

app.yaml

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

handlers:
- url: /.*
  script: _go_app

ローカルで実行

$goapp serve

ブラウザでアクセス下記アドレスにアクセス。

↓が表示されればOK!

Hello, world!

動的コンパイル

開発サーバーを起動したままhello.goを修正。
修正したらブラウザを更新。

func handler(w http.ResponseWriter, r *http.Request) {
    //fmt.Fprint(w, "Hello, World!")
    fmt.Fprint(w, "Hello, gopher!")
}

サーバーの再起動なしで修正が反映される!!
コンパイルが高速なのでまるでインタプリタ言語の様に開発出来る(^_^)

開発サーバー管理コンソール

開発サーバーの終了

Ctrl+C

Production環境にデプロイしてみよう!

GAEアプリケーションの作成

https://appengine.google.com/start

Googleアカウントでログイン。

「Create Application」をクリック。

GAEアプリケーションの作成

管理コンソール

旧(GAE Admin Console)

新(Google Developer Console)

もともとGAE単体だったサービスがGoogle Cloud Platformへ統合された。
今後は新コンソールに統一されていく流れだと思うが、まだ旧画面にしかないメニューもたくさん。

デプロイ

$goapp deploy

googleアカウントのID/passwordを入力

確認

http://your-app-id.appspot.com/

簡単なTODOアプリを作ってみよう!

仕様

ユーザー認証

GAEではGoogleアカウントによる認証を簡単に組み込むことが出来る。

FYI:

ファイル構成

helloworld/
| hello/
| | hello.go
| todo/
| | todo.go - ## new!
| app.yaml

cheat:

todo.go

import (
    "appengine"
    "appengine/user"
    "fmt"
    "net/http"
)

func init() {
    http.HandleFunc("/todo", handler)
}

todo.go

func handler(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    u := user.Current(c) // ログインユーザー取得
    if u == nil {
        // 未ログインの場合はログインURLへリダイレクト
        loginUrl, _ := user.LoginURL(c, "/todo")
        http.Redirect(w, r, loginUrl, http.StatusFound)
        return
    }
    logoutUrl, _ := user.LogoutURL(c, "/") // ログアウトURL取得

    w.Header().Set("Content-type", "text/html; charset=utf-8")

    html := `
<html><body>
Hello, %s ! - <a href="%s">sign out</a><br>
<hr>
This is TODO page under constuction!
</body></html>
`
    fmt.Fprintf(w, html, u.Email, logoutUrl)
}

開発サーバーで動作確認

$goapp serve

開発サーバーではダミー認証画面が表示される。

ログインするとTODOトップ画面へ遷移。

Production環境で動作確認

$goapp deploy

http://your-app-id.appspot.com/

TODOの登録

htmlのformから入力したTODOをGAE Datastoreに保存。

ファイル構成

helloworld/
| hello/
| | hello.go
| todo/
| | todo.go     - ## modify
| | todo.tmpl   - ## new!
| | register.go - ## new!
| app.yaml

cheat:

html/template

stringで記述していたhtmlをhtml/templateパッケージで書き換え。

FYI:

todo.go

import (
    "appengine"
    "appengine/user"
    "html/template"
    "net/http"
)

todo.go

    t, err := template.ParseFiles("todo/todo.tmpl") // テンプレート読み込み
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-type", "text/html; charset=utf-8")

    logoutUrl, _ := user.LogoutURL(c, "/")

    params := struct {
        LogoutUrl string
        User      *user.User
    }{
        logoutUrl,
        u,
    }

    err = t.Execute(w, params) // テンプレート適用!
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

todo.tmpl

<!DOCTYPE html>
<html><body>
Hello, {{.User.Email}} ! - <a href="{{.LogoutUrl}}">sign out</a><br>
<hr>
<form action="/todo/register" method="POST">
todo:<input name="Todo" type="text"></input><br>
note:<textarea name="Notes" type="text"></textarea><br>
dueDate:<input name="DueDate" type="date"></input><br>
<input type="submit"></input>
</form>
</body></html>

TODO登録処理

フォームから入力されたTODOをDatastoreに保存。

FYI:

register.go

import (
    "appengine"
    "appengine/datastore"
    "appengine/user"
    "net/http"
)

register.go

func init() {
    http.HandleFunc("/todo/register", register)
}

// Todo保存用構造体
type Todo struct {
    UserId  string
    Todo    string
    Notes   string
    DueDate string
    Done    bool
}

register.go

    if err := r.ParseForm(); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    // Entity保存用構造体を用意
    todo := Todo{
        u.ID,
        r.FormValue("Todo"),
        r.FormValue("Notes"),
        r.FormValue("DueDate"),
        false,
    }

register.go

    key := datastore.NewIncompleteKey(c, "Todo", nil)
    key, err := datastore.Put(c, key, &todo)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    http.Redirect(w, r, "/todo", http.StatusFound) // "/todo"にリダイレクト

TODOのリスト表示

保存されたTODOをリスト表示する。
Datastoreのクエリ機能を使用。

FYI:

ファイル構成

helloworld/
| hello/
| | hello.go
| todo/
| | todo.go     - ## modify
| | todo.tmpl   - ## modiry
| | register.go
| app.yaml
| inded.yaml    - ## to be generated

cheat:

todo.go

    q := datastore.NewQuery("Todo").Filter("UserId =", u.ID).Filter("Done =", false).Order("-DueDate")

    var todos []Todo
    _, err = q.GetAll(c, &todos)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

todo.go

    params := struct {
        LogoutUrl string
        User      *user.User
        Todos     []Todo
    }{
        logoutUrl,
        u,
        todos,
    }

todo.tmpl

<table border="1">
<thead>
    <tr>
        <th>Todo</th>
        <th>Notes</th>
        <th>DueDate</th>
    </tr>
</thead>
<tbody>
{{range .Todos}}
    <tr>
        <td>{{.Todo}}</td>
        <td style="white-space: pre;">{{.Notes}}</td>
        <td>{{.DueDate}}</td>
    </tr>
{{end}}
</tbody>
</table><br>

index.yaml

indexes:

# AUTOGENERATED

# This index.yaml is automatically updated whenever the dev_appserver
# detects that a new type of query is run.  If you want to manage the
# index.yaml file manually, remove the above marker line (the line
# saying "# AUTOGENERATED").  If you want to manage some indexes
# manually, move them above the marker line.  The index.yaml file is
# automatically uploaded to the admin console when you next deploy
# your application using appcfg.py.

- kind: Todo
  properties:
  - name: Done
  - name: UserId
  - name: DueDate
    direction: desc

クエリ実行に必要なインデックス定義ファイル。
開発サーバーでクエリを実行すると自動生成される。

Datastoreクエリの罠

回避策

詳細は割愛します。詳しく知りたい方は直接聞いて下さいm(_ _)m

TODOの完了

TODOを完了してリストから除外する。

ファイル構成

helloworld/
| hello/
| | hello.go
| todo/
| | done.go     - ## New!!
| | todo.go     - ## modify
| | todo.tmpl   - ## modiry
| | register.go
| app.yaml
| inded.yaml

cheat:

todo.go

    var todos []Todo
    keys, err := q.GetAll(c, &todos)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

戻り値にkeyを受け取る。
keyは更新時のパラメータとして使用する。

todo.go

    params := struct {
        LogoutUrl string
        User      *user.User
        Todos     []Todo
        Keys      []*datastore.Key
    }{
        logoutUrl,
        u,
        todos,
        keys,
    }

    err = t.Execute(w, params)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

todo.tmpl

<table border="1">
<thead>
    <tr>
        <th>Todo</th>
        <th>Notes</th>
        <th>DueDate</th>
        <th></th>
    </tr>
</thead>
<tbody>
{{$keys := .Keys}}
{{range $index, $element := .Todos}}
    <tr>
        <td>{{.Todo}}</td>
        <td style="white-space: pre;">{{.Notes}}</td>
        <td>{{.DueDate}}</td>
        <td><a href="/todo/done?key={{(index $keys $index).Encode}}">done</a></td>
    </tr>
{{end}}
</tbody>
</table><br>

done.go

    // 以下errorチェック省略

    key, err := datastore.DecodeKey(r.FormValue("key"))

    var todo Todo
    err = datastore.Get(c, key, &todo)

    if u.ID != todo.UserId {
        http.Error(w, "forbidden access.", http.StatusForbidden)
        return
    }

    todo.Done = true

    _, err = datastore.Put(c, key, &todo)

    http.Redirect(w, r, "/todo", http.StatusFound)

JSON API

JSONでtodoリストを取得するAPIを作成する。

仕様
- URLはTODO一覧取得と同じ http://your-host/todo
- Acceptヘッダが「application/json」だった場合にJSONを返す

FYI:

ファイル構成

helloworld/
| hello/
| | hello.go
| todo/
| | done.go
| | todo.go     - ## modify
| | todo.tmpl
| | register.go
| app.yaml
| inded.yaml

cheat:

todo.go

import (
    "appengine"
    "appengine/datastore"
    "appengine/user"
    "encoding/json"
    "html/template"
    "net/http"
)

todo.go

    if r.Header.Get("Accept") == "application/json" {
        w.Header().Set("Content-type", "application/json; charset=utf-8")
        b, err := json.MarshalIndent(todos, "", "\t")
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        _, err = w.Write(b)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        return
    }

確認

Postmanなどのツールで確認してみよう。

最後に

FYI:

Thank you

Daigo Ikeda

Knightso, LLC