Create a gist now

Instantly share code, notes, and snippets.

@voluntas /gae_go.rst
Last active Jul 21, 2017

What would you like to do?
GAE/Go コトハジメ

GAE/Go コトハジメ

日時:2017-07-21
作:@voluntas
バージョン:0.6.0
URL:https://voluntas.githu.io/

突っ込みは Twitter @voluntas まで。

概要

GAE/Go で自分の作りたいアプリが作れそうなので勉強してみる。GAE/Python が 2.7 系なので、 GAE/Go でせっかくだし勉強する。

かなり前に GAE/Python は触っていたが最近はまったくだし、 Golang も初心者なので、一から調べることにする。

GAE の魅力としては管理画面へのアクセスが Google 認証に簡単にできることだろう。

目的

シンプルなチケット方式サポートシステムが欲しい – V – Medium

前提

できるだけ標準ライブラリだけを利用する想定。API サーバに限定し、フロントは React で SPA を想定しているので、テンプレートエンジンは利用せず。

セッションは memcached に突っ込む、ログインは Mail ベースでの期限付きセッション生成方式。パスワードは利用しない。データベースはすべて Datastore を利用する。

管理者ログインは Google Apps ログインを利用する。

TODO

  • 静的ファイルはどうする?
  • 独自ドメイン
  • HTTPS
  • Datastore のまとめ
  • Memcache でセッション
  • メールでのログイン
    • メールアドレスが Datastore に登録されている場合、ログイン用の URL が送られてくる方式
  • 価格がどのくらいになるか

インストール

OS:macOS 10.12.5
インストール済み Python:2.7.13
インストール済み Golang:1.8.3
  • https://cloud.google.com/sdk/docs/?hl=ja
    • ダウンロードして回答したら install.sh
      • ./google-cloud-sdk/install.sh
    • その後 SDK の初期化
      • ./google-cloud-sdk/bin/gcloud init
      • 色々聞かれるので回答する
  • Go の SDK をインストールする
    • $ gcloud components install app-engine-go
  • $ gcloud components update

サンプルを動かす

URL:https://cloud.google.com/appengine/docs/standard/go/quickstart
$ git clone -b part1-helloworld https://github.com/GoogleCloudPlatform/appengine-guestbook-go.git helloworld
$ cd helloworld
$ dev_appserver.py app.yaml

http://localhost:8080/ にアクセスすると Hello, world! が見える

サンプルを Golang 1.8 で動かしてみる

app.yaml の api_version を go1.8 に切り替える

runtime: go
# api_version: go1
api_version: go1.8

handlers:
- url: /.*
  script: _go_app

デプロイしてみる

$ gcloud app deploy
$ gcloud app logs tail -s default
$ gcloud app browse

ゲストブックを動かしてみる

URL:https://cloud.google.com/appengine/docs/standard/go/getting-started/creating-guestbook
$ git clone https://github.com/GoogleCloudPlatform/appengine-guestbook-go.git
$ cd appengine-guestbook-go/
$ git fetch
$ git checkout part4-usingdatastore

$ dev_appserver.py app.yaml

http://localhost:8080/ にアクセスするとゲストブックが見える

Go の SDK を手動で入れる

goapp を使いたいので、自前で SDK をダウンロードしてきてパスを通すことにする

パスを通すと goapp が使えるようになる:

$ goapp
Go is a tool for managing Go source code.

Usage:

        goapp command [arguments]

The commands are:

        serve       starts a local development App Engine server
        deploy      deploys your application to App Engine
        build       compile packages and dependencies
        clean       remove object files
        doc         show documentation for package or symbol
        env         print Go environment information
        fix         run go tool fix on packages
        fmt         run gofmt on package sources
        generate    generate Go files by processing source
        get         download and install packages and dependencies
        install     compile and install packages and dependencies
        list        list packages
        run         compile and run Go program
        test        test packages
        tool        run specified go tool
        version     print Go version
        vet         run go tool vet on packages

Use "goapp help [command]" for more information about a command.

Additional help topics:

        c           calling between Go and C
        buildmode   description of build modes
        filetype    file types
        gopath      GOPATH environment variable
        environment environment variables
        importpath  import path syntax
        packages    description of package lists
        testflag    description of testing flags
        testfunc    description of testing functions

Use "goapp help [topic]" for more information about that topic.

ライブラリを新しくする

http://qiita.com/pside/items/72337bb5ac6571b238a4

ゲストブックアプリは古かったので 1.8 に対応したり、インポートを修正してたりしてみた。

$ goapp get google.golang.org/appengine
package guestbook

import (
        // 1.8 なので context 使うようにする
        "context"
        "html/template"
        "net/http"
        "time"

        // google.golang.org を付けるようにした
        "google.golang.org/appengine"
        "google.golang.org/appengine/datastore"
        "google.golang.org/appengine/user"
)

// [START greeting_struct]
type Greeting struct {
        Author  string
        Content string
        Date    time.Time
}

// [END greeting_struct]

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

// guestbookKey returns the key used for all guestbook entries.
// appengine.Context を context.Context に
func guestbookKey(c context.Context) *datastore.Key {
        // The string "default_guestbook" here could be varied to have multiple guestbooks.
        return datastore.NewKey(c, "Guestbook", "default_guestbook", 0, nil)
}

// [START func_root]
func root(w http.ResponseWriter, r *http.Request) {
        c := appengine.NewContext(r)
        // Ancestor queries, as shown here, are strongly consistent with the High
        // Replication Datastore. Queries that span entity groups are eventually
        // consistent. If we omitted the .Ancestor from this query there would be
        // a slight chance that Greeting that had just been written would not
        // show up in a query.
        // [START query]
        q := datastore.NewQuery("Greeting").Ancestor(guestbookKey(c)).Order("-Date").Limit(10)
        // [END query]
        // [START getall]
        greetings := make([]Greeting, 0, 10)
        if _, err := q.GetAll(c, &greetings); err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
        }
        // [END getall]
        if err := guestbookTemplate.Execute(w, greetings); err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
        }
}

// [END func_root]

var guestbookTemplate = template.Must(template.New("book").Parse(`
<html>
  <head>
    <title>Go Guestbook</title>
  </head>
  <body>
    {{range .}}
      {{with .Author}}
        <p><b>{{.}}</b> wrote:</p>
      {{else}}
        <p>An anonymous person wrote:</p>
      {{end}}
      <pre>{{.Content}}</pre>
    {{end}}
    <form action="/sign" method="post">
      <div><textarea name="content" rows="3" cols="60"></textarea></div>
      <div><input type="submit" value="Sign Guestbook"></div>
    </form>
  </body>
</html>
`))

// [START func_sign]
func sign(w http.ResponseWriter, r *http.Request) {
        // [START new_context]
        c := appengine.NewContext(r)
        // [END new_context]
        g := Greeting{
                Content: r.FormValue("content"),
                Date:    time.Now(),
        }
        // [START if_user]
        if u := user.Current(c); u != nil {
                g.Author = u.String()
        }
        // We set the same parent key on every Greeting entity to ensure each Greeting
        // is in the same entity group. Queries across the single entity group
        // will be consistent. However, the write rate to a single entity group
        // should be limited to ~1/second.
        key := datastore.NewIncompleteKey(c, "Greeting", guestbookKey(c))
        _, err := datastore.Put(c, key, &g)
        if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
        }
        http.Redirect(w, r, "/", http.StatusFound)
        // [END if_user]
}

// [END func_sign]

テストを書いてみる

guestbook でテストを書いてみる

hello_test.go というファイル名。なんとか_test.go というファイル名にする必要があるらしい

package guestbook

import (
        "net/http"
        "net/http/httptest"
        "testing"

        "google.golang.org/appengine/aetest"
)

func TestRoot(t *testing.T) {
        opt := aetest.Options{StronglyConsistentDatastore: true}
        instance, err := aetest.NewInstance(&opt)
        if err != nil {
                t.Fatalf("Failed to create aetest instance: %v", err)
        }
        defer instance.Close()

        req, _ := instance.NewRequest("GET", "/", nil)
        req.Header.Set("Content-Type", "application/json")

        res := httptest.NewRecorder()

        root(res, req)

        if res.Code != http.StatusOK {
                t.Fatalf("Non-expected status code%v:\n\tbody: %v", "200", res.Code)
        }
}

テストを実行する

goapp test でテストが実行できる。

$ goapp test
2017/07/17 21:49:31 appengine: not running under devappserver2; using some default configuration
INFO     2017-07-17 12:49:32,241 devappserver2.py:116] Skipping SDK update check.
WARNING  2017-07-17 12:49:32,241 devappserver2.py:132] DEFAULT_VERSION_HOSTNAME will not be set correctly with --port=0
INFO     2017-07-17 12:49:32,287 api_server.py:313] Starting API server at: http://localhost:54379
INFO     2017-07-17 12:49:32,291 dispatcher.py:226] Starting module "default" running at: http://localhost:54380
INFO     2017-07-17 12:49:32,293 admin_server.py:116] Starting admin server at: http://localhost:54381
PASS
ok          _/tmp/appengine-guestbook-go    3.870s

テスト速度

静的ファイル

static_dir というのを使えばなんかうまいこと行くらしい

価格

https://cloud.google.com/appengine/pricing

Datastore API

テスト

  • アカウントを作って、Datastore の StringId には Mail の値を追加
  • filter で OrgnaizationId によるフィルタリングをする
import (
        "testing"
        "time"

        "google.golang.org/appengine/aetest"
        "google.golang.org/appengine/datastore"
)

type Account struct {
        Mail             string
        Date             time.Time
        OrgnaizationName string
        OrgnaizationId   string
}

func TestDatastoreAccount(t *testing.T) {
        ctx, done, err := aetest.NewContext()
        if err != nil {
                t.Fatal(err)
        }
        defer done()

        key := datastore.NewKey(ctx, "Account", "", 0, nil)
        a := Account{
                Mail:             "me@example.com",
                Date:             time.Now(),
                OrgnaizationName: "Example Inc.",
                OrgnaizationId:   "ABC123",
        }
        key2, err := datastore.Put(ctx, key, &a)
        if err != nil {
                t.Fatal(err)
        }

        a2 := Account{}
        if err := datastore.Get(ctx, key2, &a2); err != nil {
                t.Fatal(err)
        }

        key3 := datastore.NewKey(ctx, "Account", "", 0, nil)
        a3 := Account{
                Mail:             "me@example.jp",
                Date:             time.Now(),
                OrgnaizationName: "株式会社EXAMPLE",
                OrgnaizationId:   "XYZ123",
        }
        key4, err := datastore.Put(ctx, key3, &a3)
        if err != nil {
                t.Fatal(err)
        }

        a4 := Account{}
        if err := datastore.Get(ctx, key4, &a4); err != nil {
                t.Fatal(err)
        }

        q := datastore.NewQuery("Account").Filter("OrgnaizationId =", "ABC123").Limit(10)
        // q := datastore.NewQuery("Account")
        var accounts []Account
        if _, err := q.GetAll(ctx, &accounts); err != nil {
                t.Errorf("error")
        }

        if len(accounts) != 1 {
                t.Errorf("error %d", accounts)
        }
}

トランザクション

URL:https://cloud.google.com/appengine/docs/standard/go/datastore/transactions
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment