日時: | 2017-07-21 |
---|---|
作: | @voluntas |
バージョン: | 0.6.0 |
URL: | https://voluntas.githu.io/ |
突っ込みは Twitter @voluntas まで。
Contents
GAE/Go で自分の作りたいアプリが作れそうなので勉強してみる。GAE/Python が 2.7 系なので、 GAE/Go でせっかくだし勉強する。
かなり前に GAE/Python は触っていたが最近はまったくだし、 Golang も初心者なので、一から調べることにする。
GAE の魅力としては管理画面へのアクセスが Google 認証に簡単にできることだろう。
シンプルなチケット方式サポートシステムが欲しい – V – Medium
できるだけ標準ライブラリだけを利用する想定。API サーバに限定し、フロントは React で SPA を想定しているので、テンプレートエンジンは利用せず。
セッションは memcached に突っ込む、ログインは Mail ベースでの期限付きセッション生成方式。パスワードは利用しない。データベースはすべて Datastore を利用する。
管理者ログインは Google Apps ログインを利用する。
- 静的ファイルはどうする?
- 独自ドメイン
- 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
- 色々聞かれるので回答する
- ダウンロードして回答したら install.sh
- 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! が見える
- http://qiita.com/tenntenn/items/0b92fc089f8826fabaf1
- context.Context になる話らしい
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/ にアクセスするとゲストブックが見える
goapp を使いたいので、自前で SDK をダウンロードしてきてパスを通すことにする
- https://cloud.google.com/appengine/docs/standard/go/download
- https://storage.googleapis.com/appengine-sdks/featured/go_appengine_sdk_darwin_amd64-1.9.55.zip
パスを通すと 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 でテストを書いてみる
- https://cloud.google.com/appengine/docs/standard/go/tools/localunittesting/reference
- http://qiita.com/nirasan/items/aa102285a05cd2112e4d
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
- http://ks888.hatenablog.com/entry/2016/12/23/234950
- https://blog.bulkus.net/post/luci_gae_1/
- https://github.com/luci/gae
- これがとてもいいらしい
- https://cloud.google.com/appengine/docs/standard/python/getting-started/hosting-a-static-website
- https://cloud.google.com/appengine/docs/standard/python/getting-started/serving-static-files
- https://cloud.google.com/appengine/docs/standard/python/googlecloudstorageclient/app-engine-cloud-storage-sample
static_dir というのを使えばなんかうまいこと行くらしい
https://cloud.google.com/appengine/pricing
- https://cloud.google.com/appengine/docs/standard/go/datastore/api-overview
- https://cloud.google.com/appengine/docs/standard/go/datastore/entities
- https://cloud.google.com/appengine/docs/standard/go/datastore/queries
- キーは完全に独立した概念。autoincrement とかですらない
- String ID を使うとグルーピングができるっぽい
- (継続的に追記)Datastore/Go のデータ設計のコツ - pospomeのプログラミング日記
- Datastore/Go のデータ設計と struct の振る舞いについて
- アカウントを作って、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: "[email protected]",
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: "[email protected]",
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 |
---|