GAE第二世代+Go v1.12でechoサーバーを構築する(2019年度版)

# 概要
以前もこの環境構築系の投稿をしたんですが、いずれも古いので今回また最新の**GAE第二世代**向けの構築をしてみることにしました。

なお、こちらの記事のソースコードはGitHubに公開しています。
https://github.com/brbranch/GaeGo112EchoSample/tree/gae_echo

# 2019/09/07
続編書きました。
mercari/datastoreのMemcache連携をGAEの代わりにRedis Cloudでしてみる【GAE/Go1.12】
https://qiita.com/br_branch/items/1b63b2c1dd9b4ff3931e


## 以前の記事
Golang1.8+echo+GoLandでGoogle App Engineのローカルサーバーを実行できる環境を作る
https://qiita.com/kazuked/items/a0e815fda868f302f4d1

Go言語初心者がGAE+Echo(v2)+goonでサーバーを構築する
https://qiita.com/kazuked/items/da656fd12a3177e60bb9

# 今回のゴール
* GAE/Go1.12 + echoサーバーを構築し、Datastoreにアクセスする。
* ローカルでの開発環境を構築する
* ローカルでDatastore含めたテストを行う

# 動作環境
* Mac OS 10.14
* go1.12.9
* Google Cloud SDK 253.0.0
* echo v3
* mercari/datastore v1.6.1

## GAE第二世代からはAppEngine APIは利用できなくなる
以前はgoonを使ってましたが、今回からはmercari/datastorのboomに乗り換えてみます。
https://github.com/mercari/datastore

というのも、 **GAE/go1.12(GAE第二世代) からはAppEngine APIは利用できません。**
https://cloud.google.com/appengine/docs/standard/go112/go-differences?authuser=0&hl=ja#migrating-appengine-sdk

> App Engine は、appengine パッケージを含めるように Go ツールチェーンを変更しません。appengine パッケージまたは google.golang.org/appengine パッケージを使用している場合は、Google Cloud クライアント ライブラリに移行する必要があります。
> (省略)
> App Engine で Memcache サービスを使用するには、App Engine Memcache の代わりに Redis Labs Memcached Cloud を使用します

しれっとひどいことやってきますね。
goonはAppengine APIを前提としてるっぽいので、どうも使えないようなんです。

ただ悪いことばかりでもないかもしれません。
go1.12からはApp Engineの第二世代を使うようになるらしく、何がよくなるのかよくわかってませんが、**第二世代というくらいですから何かよくなってるのかもしれない**です。

詳細：App Engine スタンダード環境のランタイム
https://cloud.google.com/appengine/docs/standard/runtimes

何が良くなりそうなのかはよくわからないけど、何か良くなってそうな気がするのは**良かった**ですね。
よかったよかった。

(きっと自由度は上がってると思う。ちなみに、AppEngine Memcacheも使えなくなるっぽい)

# まずは最小構成で動かしてみる
まずは、echoを導入してHello Worldをローカルで動かしてみるところまで作成します。

## 最初のフォルダ構成
以下の感じにしました。

```
backend (GOPATH)
├-- src (作業フォルダ)
├-- .envrc
└-- .go-version
```

## Step1. まずは最低限で動くことを確認
まずは最低限動いてデプロイできることを確認するため、srcフォルダ内にGAEのクイックスタート(helloWorld)を入れます。

https://github.com/GoogleCloudPlatform/golang-samples/tree/master/appengine/go11x/helloworld

名前だけmain.go / main_test.go とかにしちゃいます。

```bash:こんな感じにする
backend (GOPATH)
├-- src (作業フォルダ)
│   ├ app.yaml
│   ├ main.go
│   └ main_test.go
├-- .envrc
└-- .go-version
```

あとは、普通にデプロイをしてみます。

```bash
$ cd ./backend/src
$ $ gcloud app deploy
Services to deploy:

descriptor:      [/Users/xxxxx/git/my/xxxxxx/backend/src/app.yaml]
source:          [/Users/xxxxx/git/my/xxxxxx/backend/src]
target project:  [xxxxxx]
target service:  [default]
target version:  [20190906t203700]
target url:      [https://xxxxxx.appspot.com]


Do you want to continue (Y/n)?  Y

Beginning deployment of service [default]...
Created .gcloudignore file. See `gcloud topic gcloudignore` for details.
╔════════════════════════════════════════════════════════════╗
╠═ Uploading 3 files to Google Cloud Storage                ═╣
╚════════════════════════════════════════════════════════════╝
File upload done.
Updating service [default]...done.
Setting traffic split for service [default]...done.
Deployed service [default] to [https://xxxxx.appspot.com]

You can stream logs from the command line by running:
  $ gcloud app logs tail -s default

To view your application in the web browser run:
  $ gcloud app browse
```

あとは実際に動くか確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/83e26ced-0f60-d315-3d4b-18d8b185a393.png)

楽なもんですね！

## Step2. ローカルでも動かしてみる

go1.12あたりからは、ローカルでは単純に `go run` して動かせということです。
もうGAEローカルサーバーは使わないということですね。

```bash
$ go run main.go
2019/09/06 21:18:30 Defaulting to port 8080
2019/09/06 21:18:30 Listening on port 8080
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/4a92f0d7-6e5c-5808-dbd8-957810c36e93.png)

なるほどねー。

## Step3. Go Moduleを入れる
次は実際にGo Moduleを入れてデプロイをしてみます。

```bash
$ go mod init gaego112echosample
go: creating new go.mod: module gaego112echosample
$ ls
app.yaml     go.mod       main.go      main_test.go
```
とりあえず、使わないけどechoを入れます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/b915f229-469a-91e2-ae4e-f7cb8ac22945.png)

ライブラリはどこにも入ってないんで赤字になります。
これでビルドしてみます。

```bash
$ go build
go: finding github.com/labstack/echo v3.3.10+incompatible
go: downloading github.com/labstack/echo v3.3.10+incompatible
go: extracting github.com/labstack/echo v3.3.10+incompatible
go: finding github.com/labstack/gommon/log latest
go: finding github.com/labstack/gommon/color latest
︙
```

すると、こんな感じになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/5e38cf0f-a8d5-0323-a279-39eaad3bdef6.png)

※ 環境変数で `GO111MODULE=on` を設定している場合は、 `$GOPATH` で設定した場所ではなく、 `go env GOPATH` で出力される場所にパッケージがダウンロードされます。

この状態で、同じようにデプロイをしてみました。以前はdepやglide使ってる時にはvendoringの問題があったりしてエラーが発生したりしてたのですが、これでも何の問題もなくデプロイして動かすこともできました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/22ea8fa1-0ad1-f74d-c716-9bdf9ae586f9.png)

## Step4. Echoサーバーとして作成する。

じゃあ、次からはEchoを組み込んでいきます。
まずは最低限でいいですね。

```go:main.go
package main

import (
	"fmt"
	"github.com/labstack/echo"
	"log"
	"net/http"
	"os"
)

func main() {
	e := echo.New()
	http.Handle("/", e)

	e.GET("/", func (e echo.Context) error {
		return e.String(http.StatusOK, "Hello Echo!")
	})

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
		log.Printf("Defaulting to port %s", port)
	}

	log.Printf("Listening on port %s", port)
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}
```

ローカルでもデプロイしても普通に動きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/8127ceba-cdae-58f8-246d-f8b50bc5a268.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/67e9877e-590f-2d39-921a-74529666e386.png)

楽ですね。

## Step5. logを出す

上記の通り、もう `google.golang.org/appengine` は使えなくなりました。
代わりに、標準のlogパッケージを使えばいいみたいです。

それはつまり、どこからでも簡単に呼べるようになったということですね。
やってみましょう。

```go:main.go(一部抜粋)
	e.GET("/", func (e echo.Context) error {
		log.Printf("Logger test") /// 追加
		return e.String(http.StatusOK, "Hello Echo!")
	})
```

デプロイしてアクセスすると、普通にLoggingに出てきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/abc1bc0d-4693-8de1-dab9-3ef4bf700c72.png)

うん。こいつは楽ですね。

## Step6. DataStoreを扱う

さて、いよいよDataStoreを扱ってみます。あとついでに処理をパッケージで分けてもみます。
最初の方にも書いたとおり、 **第二世代のAppEngineではAppEngine APIを使えない** という逆説的なことになってますので、今までお世話になってた `google.golang.org/appengine/datastore` はもう使えません。
代わりに、 `cloud.google.com/go/datastore` を利用し、 メルカリが作ってる `boom` を使わせてもらいます。

```go:src/handler/hello_handler.go
package handler

import (
	"cloud.google.com/go/datastore"
	"github.com/labstack/echo"
	"go.mercari.io/datastore/boom"
	"go.mercari.io/datastore/clouddatastore"
	"log"
	"net/http"
	"os"
)

type Post struct {
	Kind	string `datastore:"-" boom:"kind,post" json:"-"`
	ID		int64 `datastore:"-" boom:"id" json:"id"`
	Content	string `datastore:"content" json:"content"`
}

func HelloWorld(e echo.Context) error {
	// app.yaml などにPROJECT_IDを設定しておく
	projectId := os.Getenv("PROJECT_ID")
	log.Printf("Project ID: %s", projectId)
	// DataStore Clientの作成
	ctx := e.Request().Context()
	dataClient, err := datastore.NewClient(ctx, projectId)
	if err != nil {
		log.Fatalf("failed to get client (reason: %v)", err)
		return e.String(http.StatusInternalServerError, "error")
	}
	// mercari.datastoreでラップする
	client, err := clouddatastore.FromClient(ctx, dataClient)
	if err != nil {
		log.Fatalf("failed to get datastoreclient (reason: %v)", err)
		return e.String(http.StatusInternalServerError, "error")
	}
	defer client.Close()
	// boomを利用
	b := boom.FromClient(ctx, client)
	post := &Post{ID: 12345, Content:"test"}
	// 保存
	if _, err := b.Put(post); err != nil {
		log.Fatalf("failed to put datastore (reason: %v)", err)
		return e.String(http.StatusInternalServerError, "error")
	}
	// 取得
	getPost := &Post{ID: 12345}
	if err := b.Get(getPost); err != nil {
		log.Fatalf("failed to get datastore (reason: %v)", err)
		return e.String(http.StatusInternalServerError, "error")
	}

	return e.JSON(http.StatusOK, getPost)
}
```
```go:main.go
// 抜粋
e.GET("/", handler.HelloWorld)
```

これでローカル環境でも、GAEの環境でもGCP/DataStoreに保存 / 取得することができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/16ecc594-f817-2efe-52fa-adfc863ec18d.png)

どうも、 gloudでログインしているユーザーにアクセス権限があれば、ローカル環境からも自動でつなぐみたいですね。

## Step7. ローカルのDataStore Emulator にアクセスする
これでも開発はできるんですが、せっかくならローカルのエミュレーターにつなぎたいですよね。
というわけで、それの設定もしてみます。

### ローカルのエミュレーターを起動
以下のコマンドでできます。
`--project` の部分は `[a-z][a-z0-9\-]+` の命名規則であれば任意でも問題ないようです。指定しなければ `gcloud` で設定している current project id が入ります。

```bash
$ gcloud beta emulators datastore start --host-port localhost:8059 --project test-project
WARNING: Reusing existing data in [/Users/xxxx/.config/gcloud/emulators/datastore].
Executing: /Users/xxxx/tools/google-cloud-sdk/platform/cloud-datastore-emulator/cloud_datastore_emulator start --host=localhost --port=8059 --store_on_disk=True --consistency=0.9 --allow_remote_shutdown /Users/xxxx/.config/gcloud/emulators/datastore
[datastore] 9 07, 2019 2:00:18 午後 com.google.cloud.datastore.emulator.CloudDatastore$FakeDatastoreAction$9 apply
[datastore] 情報: Provided --allow_remote_shutdown to start command which is no longer necessary.
[datastore] 9 07, 2019 2:00:18 午後 com.google.cloud.datastore.emulator.impl.LocalDatastoreFileStub <init>
[datastore] 情報: Local Datastore initialized:
[datastore] 	Type: High Replication
[datastore] 	Storage: /Users/xxxx/.config/gcloud/emulators/datastore/WEB-INF/appengine-generated/local_db.bin
[datastore] 9 07, 2019 2:00:18 午後 com.google.cloud.datastore.emulator.impl.LocalDatastoreFileStub load
[datastore] 情報: The backing store, /Users/xxxx/.config/gcloud/emulators/datastore/WEB-INF/appengine-generated/local_db.bin, does not exist. It will be created.
[datastore] API endpoint: http://localhost:8059
[datastore] If you are using a library that supports the DATASTORE_EMULATOR_HOST environment variable, run:
[datastore]
[datastore]   export DATASTORE_EMULATOR_HOST=localhost:8059
[datastore]
[datastore] Dev App Server is now running.
[datastore]
[datastore] The previous line was printed for backwards compatibility only.
[datastore] If your tests rely on it to confirm emulator startup,
[datastore] please migrate to the emulator health check endpoint (/). Thank you!
```

### 実際に動かしてみる

上記のエミュレーターを起動した状態で、ローカルで連携をしてみます。
以下のコマンドを付与して `run` します。

```bash
$ env DATASTORE_EMULATOR_HOST=localhost:8059 DATASTORE_PROJECT_ID=test-project go run main.go
```
そして `localhost:8080` にアクセスすると正常に表示され、エミュレーターを起動した側のターミナル上にログが出力されます。

```bash
[datastore] The health check endpoint for this emulator instance is http://localhost:8059/9 07, 2019 2:06:34 午後 io.gapi.emulators.grpc.GrpcServer$3 operationComplete
[datastore] 情報: Adding handler(s) to newly registered Channel.
[datastore] 9 07, 2019 2:06:34 午後 io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
[datastore] 情報: Detected HTTP/2 connection.
[datastore]
[datastore]
[datastore] 9 07, 2019 2:06:48 午後 com.google.cloud.datastore.emulator.impl.LocalDatastoreFileStub lambda$persist$7
[datastore] 情報: Time to persist datastore: 27 ms
```

これでローカル環境での開発も捗りそうです(`・ω・´)

### ローカルのエミュレーターの中を確認(Google Cloud GUI)
ただ、エミュレーターの場合今までのように Appengine Local Serverの管理画面からDataStoreの中を見るということができません。ちょっと気になったりすることもあるんで、それも見れるようにします。

すごい人が作ってたので、それを利用します。

Google Cloud GUI
https://github.com/GabiAxel/google-cloud-gui

```bash
$ npm i -g google-cloud-gui
$ google-cloud-gui
```
上記を実行すると、ブラウザが自動で立ち上がります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/852bbad9-d3f1-57d2-c89f-ac0b4558f84b.png)

あとは「Projects」の左にある+ボタンを押してプロジェクトを追加すると、確認することができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/294312be-4a48-80d2-3594-286348810ddb.png)

素敵。
(ターミナル見ると何やらたくさんエラーはでてるけど、使えてるのでOKですね)

## Step8. テストケースを書く
いよいよゴール間近です。
今回はとりあえず、DataStore Emulatorを使って、実際にDataStoreに接続する形でのテストを記述してみます。

```go:handler/hello_handler_test.go
func TestHelloWorld(t *testing.T) {
	// 環境変数をセット
	os.Setenv("DATASTORE_PROJECT_ID", "test-project")
	os.Setenv("DATASTORE_EMULATOR_HOST", "localhost:8059")

	e := echo.New()
	req, err := http.NewRequest("GET", "/", nil)
	if err != nil {
		t.Fatal(err)
	}

	rr := httptest.NewRecorder()

	c := e.NewContext(req, rr)
	err = HelloWorld(c)
	if err != nil {
		t.Errorf("unexpeced handler reponse (err: %v)", err)
		return
	}

	if status := rr.Code; status != http.StatusOK {
		t.Errorf(
			"unexpected status: got (%v) want (%v)",
			status,
			http.StatusOK,
		)
	}

	expected := `{"id":12345,"content":"test"}`
	if strings.TrimSpace(rr.Body.String()) != expected {
		t.Errorf(
			"unexpected body: got (%v) want (%v)",
			rr.Body.String(),
			expected,
		)
	}
}
```
DataStore Emulatorを起動した状態で実行すると、無事テストはパスしました。

```
=== RUN   TestHelloWorld
2019/09/07 14:51:33 Project ID: 
--- PASS: TestHelloWorld (0.02s)
PASS
```

# やってみた感想

とりあえず、構築自体はそんなに引っかかることなくできました。
Go Moduleでvendoringから開放されるのは嬉しいです。
ただ、最初にも書いた **GAE/go1.12 からはAppEngine APIは利用できない**って結構えげつないですね。
既存プロジェクトの変更の影響かなり大きそう・・・。

## まだできてないこと

上記の感じなので、今はDataStoreとmemcacheの連携はできてないはず。
(mercari/datastoreがすごく優秀でローカルキャッシュもやってくれてるみたいな記事をどこかで読んだんだけど、その動作の確認はできてないです)
「**今後は Appengine Memcache ではなく Redis Cloudを使え**」とGCPもおっしゃってるんで、次回はその連携もしてみようと思います。
Redis Cloud は30MBまでは無料らしいです。

30MBかぁ…(´・_・`)
個人でやるとしても、実際に運用する際には多少の課金は必要になってきそう。

# トラブルシューティング
構築中に発生したエラーをまとめておきます。

## goenvでgo1.12をインストールできない
goのバージョン管理で `goenv` を使っているのですが、ぼくの環境では `install --list` でも表示されず、最初うまく入れることができませんでした。

```bash
$ goenv install 1.12.9
go-build: definition not found: 1.12.9

See all available versions with `goenv install --list'.

If the version you need is missing, try upgrading goenv:

  brew update && brew upgrade goenv
```

### 解決方法
以下のようにしてアップデートをしました。

```bash
$ brew update # brew自体をアップデートする
$ $ brew install --HEAD goenv
Error: Xcode alone is not sufficient on Mojave.
Install the Command Line Tools:
  xcode-select --install
# 怒られたんで、言われるままにやります（というか入ってなかったのか…)
$ xcode-select --install
xcode-select: note: install requested for command line developer tools
$ brew install --HEAD goenv
Error: goenv 1.23.3 is already installed
To install HEAD, first run `brew unlink goenv`.
# また怒られた。言われた通りにunlinkする
$ $ brew unlink goenv
Unlinking /usr/local/Cellar/goenv/1.23.3... 4 symlinks removed
$ brew install --HEAD goenv
# 成功!
```

## Mac OS 10.14 だとgo1.12でテストできない
なぜか、go test をやろうとするとこんなエラーがでちゃいます。

```bash
$ go test
go: creating new go.mod: module github.com/brbranch/JankenShogiOnline
# runtime/cgo
In file included from gcc_libinit.c:8:
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/pthread.h:232:66: error: unknown type name 'size_t'
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/pthread.h:249:43: error: unknown type name 'size_t'
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/pthread.h:256:66: error: unknown type name 'size_t'
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/pthread.h:524:1: error: unknown type name 'size_t'
gcc_libinit.c:97:18: error: variable has incomplete type 'struct timespec'
gcc_libinit.c:97:9: note: forward declaration of 'struct timespec'
gcc_libinit.c:110:3: error: implicit declaration of function 'nanosleep' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
FAIL	github.com/xxx/xxxx/backend/src [build failed]
```

### 解決方法
https://github.com/golang/go/issues/30072 に載ってました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/1764b5cb-cc15-06b6-166e-bee59103b542.png)

よくわかってないけど、 以下の方法で実行したらテストできました。

```
$ env CGO_ENABLED=0 go test
PASS
ok  	github.com/xxxx/xxxx/backend/src	0.013s
```
なんかなっとくいかないけど、動いてるから正義ってことで(´・ω・`)

# 参考
App Engine スタンダード環境のランタイム
https://cloud.google.com/appengine/docs/standard/runtimes

Migrating your App Engine app to Go 1.12
https://cloud.google.com/appengine/docs/standard/go112/go-differences

Google Cloud GUI
https://github.com/GabiAxel/google-cloud-gui

mercari/datastore の知見を語っておく
https://qiita.com/vvakame/items/f24444267a0ce786dffc

GAE+GO+Echoでgo1.11対応した
https://qiita.com/nyappa/items/dccac12e6332d311e37b