Go言語初心者がGAE+Echo(v2)+goonでサーバーを構築する

# この記事を三行でいうと
* ゼロからGo開発環境を構築
* Echo(v2.0)+goonでWebサーバーを構築してGAEローカルサーバーで動かしてみた
* 何かテストでいいノウハウがあったら教えてください

## 追記(2019/09/08)
最新版も記載しました。
GAE第二世代+Go v1.12でechoサーバーを構築する(2019年度版)
https://qiita.com/br_branch/items/a26480a05ecb97ac20b3

# 記事の内容
Go言語をいじりだして1ヶ月目の初心者です。掲題の環境で開発をする仕事がまわってきたのですが、途中から参画したこともあってよくわからないままこれまでやってきました。ただ、それだとちょっと気持ち悪かったので、学習も兼ねてゼロから構築してみた話をつらつら書いていきます。
WebアプリケーションのフレームワークはEchoのv2を使います。また、Google Cloud Datastoreを簡単に操作できるgoonを使ってデータ操作をします。

※自分の備忘録も兼ねてるので無駄な記述もありますがご了承ください。

# やったこと
* (Mac OSXでの)Go開発環境の構築
   * Go 1.8.3
   * Gogland
   * App Engine SDK for Go
   * direnv
   * glide
* Echo+goonでのHello world
* テストしやすい設計の検討（答え出てない）

# Go開発環境の構築
## この節のゴール
まずは(後々Echo + goon で作成することは考慮にいれつつ) [Google App Engine Go Standard Environment ドキュメント](https://cloud.google.com/appengine/docs/go/?hl=ja) にあるサンプルソースがローカル環境上で動くようになるまでを目指します。
※ ただし環境はMac OS v10.12 Sierraです

## プロジェクトのディレクトリ構成
以下のディレクトリ構成の前提で話を進めていきます。

* /ProjectDir
    * .envrc
    * app.yaml
    * main.go

## Go言語のインストール
これは簡単ですね。 [Go言語の公式サイト](http://golang-jp.org/)からインストールすれば完了です。とりあえず**1.8.3**の現時点での最新版にしました。
GAEはGo言語の**1.6**で動いているらしい(と構築後に気づいた)ので、合わせたほうがいいかなと思ったのですが、今のところは特に問題なく動いてるのでこのままにしてます。
ちなみに、Go言語のバージョン管理で [goenv](https://github.com/syndbg/goenv) というのもあるらしい。まあ必要になったら入れようと思います。

## Goglandのインストールと設定
[Gogland](https://www.jetbrains.com/go/)はJetBrains社が2016年に出したGo言語用のIDEですね。今のところ無料で使えるので使ってます。
インストール後は、GOPATHを設定するだけでもう開発できちゃいます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/163629/bf9b4154-1230-17c1-4b58-1e85eed94bcd.png)
※ `GOPATH` は `/ProjectDir` の絶対パスを選択

## App Engine SDK For Go
Go言語で作成したプロジェクトをApp EngineにデプロイするためのSDKです。その他、開発用に、ローカル環境に擬似的にAppEngineの環境（MemcacheやDatastore)を立ち上げてくれるツールなども存在します。
以前は `appcfg.py`というSDK内のスクリプトでデプロイをしていたそうですが、最近は　`gcloud` 内のコマンドに統合されたそうです。なので、おそらく`appcfg.py`は非推奨になるのだと思うのですが、**色々試してみたもののgcloudではEcho+goon+glideで作成したプロジェクトのデプロイに失敗する**ので以前のを使って構築します(だれかgloudでデプロイする方法知ってる人いたら教えて＞＜）

これも簡単ですね。 [Quickstart for Go App Engine Standard Environment](https://cloud.google.com/appengine/docs/standard/go/quickstart?hl=ja)の"Download The SDK"に従ってとってくるだけです。
ただ、上にも書いたように今回は`appcfg.py`を利用するため、 **Google Cloud SDK**だけじゃなく、**Optional: Download and install the original App Engine SDK for Go**の方もダウンロード＆インストールします（両方必要なのかどうかは不明）。

## direnvのインストール
[direnv](https://github.com/direnv/direnv)とは、対象のディレクトリをカレントディレクトリにした際に環境変数を自動で書き換えてくれる君です。Go言語の場合、GOPATHの環境変数を設定しないと動かなかったりするので、この子を使うととても便利っぽい。

インストールは、Macの場合以下だけでできます(Homebrewを入れてるの前提)。

```bash
$ brew install direnv
```
インストール後、`.bash_profile` に以下の設定をします。

```bash
# エディタはお好きなものでOK
export EDITOR=/usr/bin/vim
eval "$(direnv hook bash)"
```
また、/ProjectDirで以下のコマンドを打ち、**.envrc**ファイルを作成します。

```bash
$ cd ${/ProjectDir}
$ direnv edit .
# 上記設定の場合Vimが立ち上がるので、以下入力
export GOPATH=$(pwd)
```

## glideのインストール
[glide](https://github.com/Masterminds/glide)はGo言語用の各プロジェクトごとのパッケージ管理らしいです。
プロジェクトごとの、とあえていうのは、Go言語には言語仕様でパッケージ管理っぽいことができるらしい(go getのことなのかな？ まだよくわかってない。。。)のですが、その場合globalとなるため、色々アレだよねっていうのでできたらしいです。

インストールは以下できます。そう、Macならね。

```bash
$ brew install glide
```
まだインストールするだけで、特に何も設定はしません。

## サンプルプロジェクトのダウンロードと動作確認
本当はglideとかdirenvとかは入れなくてもサンプルプロジェクトは動くのですが。
サンプルプロジェクトと動かし方は [Googleさんのクイックスタート](https://cloud.google.com/appengine/docs/standard/go/quickstart?hl=ja)に書いてあるので、ここでは割愛します。
(**Download the Hello World app**と**Test the application**をやるだけで動きますね）

# Echo v2.0とGoonでHello Worldを作成する
開発するための環境ができあがったら、Echoとgoonを使って簡単なWebアプリケーションを作ってみます。
## ここからのディレクトリ構成
以下のディレクトリ構成の前提で話を進めていきます。

* /ProjectDir
    * .envrc
    * app.yaml
    * /src
        * glide.yaml
        * glide.lock　(glideで作成される)
        * /backend (ソースコード配置)
        * /maintest (テストコード配置)
        * /vendor (glideで作成される)

別にcommonファイルは今回扱いません(いちおうsubmoduleで分けるならsrc直下の方がやりやすそうだよっていう自分の備忘のために描いてるだけです)。

## glide.yamlの作成
今回はEchoとgoonを利用するので、以下だけは作りはじめる前にいれておきます。
また、appengineのログなども出したいので、同じように入れておきます。
testeratorは、テストを高速にするライブラリらしいです。入れですね。

```
package: .
import:
- package: github.com/labstack/echo
  version: v2
  subpackages:
      - engine/standard
- package: github.com/mjibson/goon
- package: golang.org/x/net
  subpackages:
      - context
- package: google.golang.org/appengine
  subpackages:
      - log
- package: github.com/favclip/testerator
```
今回、ぼくは直接vimでglide.yamlを作成したのですが、glideには自動で作成するコマンドもあります。ただ自動生成の場合、既存のソースから必要とされてるパッケージを記述するって形らしいので、goファイルがないとエラーになるっぽいです。

依存関係のダウンロードは以下のコマンドを打つだけです。

```bash
$ cd ${/ProjectDir/src} # glide.yamlのあるディレクトリへ行く
$ glide up
```

すると、vendorのディレクトリが同一階層上に作成されて依存ファイルがぬるぬる入ってきます。
別の記事では「**glide.yamlは実際のソースファイルを配置する場所(ここだと/main)に入れないとAppEngineで動かないよ！**」って書き込みがあったのですが、今のところ普通に動いてるのでこのディレクトリ構成でも問題ないんじゃないかな。たぶん。
（このディレクトリ構成ならテスト時にも利用できるし）

## app.yamlの編集
実際作り出す前に、 AppEngineに配置する際の設定を行います。

```
runtime: go
api_version: go1

handlers:
    - url: /.*
      script: _go_app
      secure: optional

automatic_scaling:
    max_concurrent_requests: 40
    min_idle_instances: 0
    max_idle_instances: 1

skip_files:
    - \.gitignore
    - \.DS_Store
    - \.envrc
    - README.md
    - ^.*\.yaml
    - \.git/.*
    - ^\.idea/.*
    - ^.*\.iml
    - src/vendor
    - src/test
```
たぶん重要なのが `src/vendor` をスキップにすることかなと思います。Echoの場合、AppEngineが許容していないライブラリを内部で利用しているため、それをスキップしないと怒られるっぽいです。
（どうしてスキップしても動いてるのかはよくわかんない。。。)

## 実際に作成する
長い設定も終わりです。というわけで、実際に簡単なのを作成してみます。
実際はクラスを分けてそれぞれに役割を分担させたほうがいいのだろうけど、面倒なので、/src/main内にmain.goだけを作成し、そこに全部突っ込みます。

```go
package backend

import (
	"github.com/labstack/echo"
	"github.com/labstack/echo/middleware"
	"github.com/labstack/echo/engine/standard"
	"google.golang.org/appengine"
	"google.golang.org/appengine/log"
	"github.com/mjibson/goon"
	"net/http"
	"fmt"
	"strconv"
	"encoding/json"
)

type (
	TestEntity struct {
		_kind string `goon:"kind,TestEntity"`
		Id int64 `datastore:"-" goon:"id" json:"id"`
		Name string `datastore:"name,noindex" json:"name"`
	}
)

// 最初に呼ばれる箇所
func init() {
	e := echo.New()
        // ミドルウェアの設定
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())
	e.Use(middleware.Gzip())
	e.Use(UseAppEngine)

	// Entityを作成または更新する
	e.PUT("/entity/:id/:name", func(e echo.Context) error {
		id , err := strconv.ParseInt(e.Param("id"), 10 , 64)
		if err != nil {
			log.Warningf(e.StdContext() , "failed to parseInt (id:%s).",e.Param("id"))
			return e.String(http.StatusNotFound , "")
		}
		name := e.Param("name")
		entity := &TestEntity{Id:id , Name:name}
		db := goon.FromContext(e.StdContext())
		if _, err := db.Put(entity); err != nil {
			log.Errorf(e.StdContext() , "failed to create entity.")
			return e.String(http.StatusInternalServerError, "failded to create entity.")
		}
		return e.String(http.StatusOK , fmt.Sprintf("create user (id:%d name:%s)",id, name))
	})

	// Entityを取得する
	e.GET("/entity/:id", func(e echo.Context) error {
		id , err := strconv.ParseInt(e.Param("id"), 10 , 64)
		if err != nil {
			log.Warningf(e.StdContext() , "failed to parseInt (id:%s).",e.Param("id"))
			return e.String(http.StatusNotFound , "")
		}
		db := goon.FromContext(e.StdContext())
		entity := &TestEntity{Id:int64(id)}
		if err := db.Get(entity); err != nil{
			log.Warningf(e.StdContext() , "entity not found (id : %d)", id)
			return e.String(http.StatusNotFound , fmt.Sprintf("entity not found (id : %d)", id))
		}
		result , err := json.Marshal(entity)
		if err != nil {
			log.Errorf(e.StdContext() , "failed to marshal entity.")
			return e.String(http.StatusInternalServerError, fmt.Sprintf("failed to marshal (err: %v)", err))
		}

		return e.String(http.StatusOK , string(result))

	})

	s := standard.New("")
	s.SetHandler(e)
	http.Handle("/", s)
}

// AppEngineを利用できるコンテキストを設定する
func UseAppEngine (next echo.HandlerFunc) echo.HandlerFunc {
	return func(c echo.Context) error {
		if r, ok := c.Request().(*standard.Request); ok {
			namespace := "development"
			ctx := appengine.WithContext(c.StdContext(), r.Request)
			ctx , err := appengine.Namespace(ctx, namespace)
			if err != nil {
				log.Errorf(ctx, "unresolve to set namespace (err %v)", err)
			}
			log.Infof(ctx , "namespace:%s", namespace)
			c.SetStdContext(ctx)
		}
		return next(c)
	}
}
```
ローカルサーバー上に起動する際にはAppEngine SDKを利用し、以下のようにします。

```bash
$ cd ${/ProjectDir}
$ dev_appserver.py app.yaml 
```
動作確認してみます。

```bash
$ curl -i -X PUT -s 'localhost:8080/entity/1/abc'
HTTP/1.1 200 OK
vary: Accept-Encoding
content-type: text/plain; charset=utf-8
Cache-Control: no-cache
Expires: Fri, 01 Jan 1990 00:00:00 GMT
Content-Length: 27
Server: Development/2.0
Date: Sun, 23 Jul 2017 14:14:08 GMT

create user (id:1 name:abc)

$ curl -i -X GET -s 'localhost:8080/entity/1'
HTTP/1.1 200 OK
vary: Accept-Encoding
content-type: text/plain; charset=utf-8
Cache-Control: no-cache
Expires: Fri, 01 Jan 1990 00:00:00 GMT
Content-Length: 21
Server: Development/2.0
Date: Sun, 23 Jul 2017 14:15:07 GMT

{"id":1,"name":"abc"}
```

# 雑感
結構簡単に環境構築も実装もできることは今回最初から構築してみてわかってきました。
ただ、GAEで動かす前提の設計だと、テストが少し面倒になっちゃいますね。。

少なくともビジネスロジック部分はサーバーから切り離してテストしたいのですが、その場合goonをインタフェースでラップし、Dependency Injectionパターンで作るような設計にするのがいいのかなぁ。Echoもgoonもテスト設計というのがないような気がするというか、そもそもGoのインタフェースって癖があると感じちゃうというか、なんというか。

進捗ダメです。