GAE/Go + EchoでFirestoreを導入してみた

# 概要
まだGCP上で既存プロジェクトを扱う際にはDataStoreをそのまま使うということも多いけど、新しいプロジェクトを作成する際にはやっぱりFirestoreを使っていきたいよねー。

そんなわけで、構築＆動作確認をした際のメモ書きです。

参考： 
Go + Firestore (サーバー側) でローカルエミュレーターを使ってテストする
https://qiita.com/castaneai/items/c7d68cbee1a6e3655247

## 開発環境
環境は以下の通りです。

- Mac OSX(10.14.5)
- Golang 1.9
- Echo 3.1.0
- AppEngine 1.6.1

# 環境構築編
## gcloud componentsのアップデート
あまり調べてないけど、ぼくが入れていた[gcloud](https://cloud.google.com/sdk/)のバージョンでは対応してなかったので、コンポーネントの更新を行いました。

```bash
$ gcloud components update

Your current Cloud SDK version is: 206.0.0
You will be upgraded to version: 253.0.0

┌─────────────────────────────────────────────────────────────────────────────┐
│                      These components will be updated.                      │
├─────────────────────────────────────────────────────┬────────────┬──────────┤
│                         Name                        │  Version   │   Size   │
├─────────────────────────────────────────────────────┼────────────┼──────────┤
│ App Engine Go Extensions                            │     1.9.70 │ 56.4 MiB │
│ BigQuery Command Line Tool                          │     2.0.44 │  < 1 MiB │
│ BigQuery Command Line Tool (Platform Specific)      │     2.0.34 │  < 1 MiB │
│ Cloud SDK Core Libraries                            │ 2019.06.28 │ 11.0 MiB │
│ Cloud SDK Core Libraries (Platform Specific)        │ 2018.09.24 │  < 1 MiB │
│ Cloud Storage Command Line Tool                     │       4.39 │  3.6 MiB │
│ Cloud Storage Command Line Tool (Platform Specific) │       4.34 │  < 1 MiB │
│ gRPC python library                                 │     1.20.0 │  1.9 MiB │
│ gcloud Beta Commands                                │ 2019.05.17 │  < 1 MiB │
│ gcloud app Python Extensions                        │     1.9.86 │  6.0 MiB │
│ gcloud cli dependencies                             │ 2019.05.03 │  2.4 MiB │
│ gcloud cli dependencies                             │ 2018.08.03 │  1.5 MiB │
└─────────────────────────────────────────────────────┴────────────┴──────────┘
┌───────────────────────────────────────────────┐
│      These components will be installed.      │
├──────────────────────────┬─────────┬──────────┤
│           Name           │ Version │   Size   │
├──────────────────────────┼─────────┼──────────┤
│ Cloud Datastore Emulator │   2.1.0 │ 18.4 MiB │
└──────────────────────────┴─────────┴──────────┘

A lot has changed since your last upgrade.  For the latest full release notes,
please visit:
  https://cloud.google.com/sdk/release_notes

Do you want to continue (Y/n)?  Y
# あとは省略
```

### goappを使えるようにする
gcloudを更新すると、Macだとだいたいgoappが使えなくなっちゃいます。
(本当は `goapp` をやめてgcloudで完結させたいんだけど、面倒だから今もgoappでローカル検証 + deployしてます)

```bash
$ goapp serve app-sandbox.yaml
-bash: /Users/xxx/tools/google-cloud-sdk/platform/google_appengine/goapp: Permission denied
```
こんな感じにエラーになっちゃうので、いつものように権限を通します。

```bash
$ sudo chmod +x ~/tools/google-cloud-sdk/platform/google_appengine/goapp
# 使えるようになったことを確認
$ goapp
Go is a tool for managing Go source code.

Usage:

	goapp command [arguments]

The commands are: (省略)
```

### firestore emuratorを起動

はじめて使う場合、エミュレーターがダウンロードされます。
今後はローカルで検証する際には毎回 firestoreのエミュレーターを起動することになるみたい。

```bash
$  gcloud beta emulators firestore start
You need the [cloud-firestore-emulator] component to use the Google
Cloud Firestore emulator.


Your current Cloud SDK version is: 253.0.0
Installing components from version: 253.0.0

┌───────────────────────────────────────────────┐
│      These components will be installed.      │
├──────────────────────────┬─────────┬──────────┤
│           Name           │ Version │   Size   │
├──────────────────────────┼─────────┼──────────┤
│ Cloud Firestore Emulator │   1.6.0 │ 41.1 MiB │
└──────────────────────────┴─────────┴──────────┘

For the latest full release notes, please visit:
  https://cloud.google.com/sdk/release_notes

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

╔════════════════════════════════════════════════════════════╗
╠═ Creating update staging area                             ═╣
╠════════════════════════════════════════════════════════════╣
╠═ Installing: Cloud Firestore Emulator                     ═╣
╠════════════════════════════════════════════════════════════╣
╠═ Creating backup and activating new installation          ═╣
╚════════════════════════════════════════════════════════════╝

Performing post processing steps...done.

Update done!

Restarting command:
  $ gcloud beta emulators firestore start

Executing: /Users/xxx/tools/google-cloud-sdk/platform/cloud-firestore-emulator/cloud_firestore_emulator start --host=::1 --port=8915
[firestore] API endpoint: http://::1:8915
[firestore] If you are using a library that supports the FIRESTORE_EMULATOR_HOST environment variable, run:
[firestore]
[firestore]    export FIRESTORE_EMULATOR_HOST=::1:8915
[firestore]
[firestore] Dev App Server is now running.
```

# 実際に動かしてみる
## yamlファイルの修正

FIRESTORE_EMULATOR_HOST の定数を入れろということなので、　yamlファイルに以下のように記載。

```app.yaml
application: testProject
module: default
version: 1
runtime: go
api_version: go1.9
threadsafe: true

handlers:
    - url: /.*
      script: _go_app

skip_files:
    - (.*/)?vendor/(.*/)?
    - (.*/)?golang\.org/(.*/)?
    - (.*/)?pkg/(.*/)?

automatic_scaling:
    max_concurrent_requests: 40
    min_idle_instances: 0
    max_idle_instances: 1
    max_pending_latency: automatic
    min_pending_latency: 3000ms

env_variables:
  FIRESTORE_EMULATOR_HOST: "localhost:8915" # 追加
```

## 動作確認

とりあえずは動けばいいレベルで。
`GET /debug` を叩くと登録され、 `GET /debug/{id}` を叩くとそのIDのものが取得できるだけのやつ。

```go:main.go

import (
	"context"
	"github.com/labstack/echo"
	"google.golang.org/appengine/log"
	"google.golang.org/appengine"
	"net/http"
	"project/app/core"
	"project/app/handler"
	"github.com/gorilla/sessions"
	"github.com/labstack/echo-contrib/session"
	"project/app/middlewares"
	"firebase.google.com/go"
	"fmt"
)


const AppEngine = "appengine"
var e = createMux()

func init() {
	{
		ah := e.Group("/_ah")
		ah.GET("/start", func(c echo.Context) error { return c.String(http.StatusOK, "") })
	}


	g := e.Group("/debug")
	g.GET("", func(c echo.Context) error {
		ctx, ok := c.Get(AppEngine).(context.Context)
		if !ok {
			return c.String(http.StatusInternalServerError, "error")
		}
		app, err := firebase.NewApp(ctx, nil)
		if err != nil {
			return c.String(http.StatusInternalServerError, fmt.Sprintf("failed (reason: %+v)", err))
		}
		store, err := app.Firestore(ctx)
		if err != nil {
			return c.String(http.StatusInternalServerError, fmt.Sprintf("failed (reason: %+v)", err))
		}
		doc, _, err := store.Collection("users").Add(ctx, map[string]interface{}{
			"name": "hello",
		});
		if err != nil {
			return c.String(http.StatusInternalServerError, fmt.Sprintf("failed (reason: %+v)", err))
		}

		return c.String(http.StatusOK, fmt.Sprintf("DocID: %s", doc.ID))
	});

	g.GET("/:id", func(c echo.Context) error {
		ctx, ok := c.Get(AppEngine).(context.Context)
		if !ok {
			return c.String(http.StatusInternalServerError, "error")
		}
		app, err := firebase.NewApp(ctx, nil)
		if err != nil {
			return c.String(http.StatusInternalServerError, fmt.Sprintf("failed (reason: %+v)", err))
		}
		store, err := app.Firestore(ctx)
		if err != nil {
			return c.String(http.StatusInternalServerError, fmt.Sprintf("failed (reason: %+v)", err))
		}
		dsnap, err := store.Collection("users").Doc(c.Param("id")).Get(ctx)
		if err != nil {
			return c.String(http.StatusInternalServerError, fmt.Sprintf("failed (reason: %+v)", err))
		}
		return c.String(http.StatusOK, fmt.Sprintf("DocID: %+v", dsnap.Data()));
	})

}

func UseAppEngine() echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			ctx := appengine.NewContext(c.Request())
			c.Set(AppEngine, ctx)
			return next(c)
		}
	}
}

func createMux() *echo.Echo {
	e := echo.New()
	e.Use(core.UseAppEngine())
	http.Handle("/", e)
	return e
}
```
### GET /debug を叩く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/2ac2a2e0-f7e1-1086-d2e0-505166db2211.png)


### GET /debug/:id を叩く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/41c71b32-7c33-ed31-8716-bba25060b157.png)

なるほどねー。

# 所感

導入自体は特に何もつまるところはなかったです。 firestoreのAPIの学習はこれからなのでよくわかってないです(´・_・`)
一応 DataStoreと互換性あるって話だけど、 [goon](https://godoc.org/github.com/mjibson/goon) は使えないんだろうなぁ。