この記事を三行でいうと
- ゼロから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 ドキュメント にあるサンプルソースがローカル環境上で動くようになるまでを目指します。
※ ただし環境はMac OS v10.12 Sierraです
プロジェクトのディレクトリ構成
以下のディレクトリ構成の前提で話を進めていきます。
- /ProjectDir
- .envrc
- app.yaml
- main.go
Go言語のインストール
これは簡単ですね。 Go言語の公式サイトからインストールすれば完了です。とりあえず1.8.3の現時点での最新版にしました。
GAEはGo言語の1.6で動いているらしい(と構築後に気づいた)ので、合わせたほうがいいかなと思ったのですが、今のところは特に問題なく動いてるのでこのままにしてます。
ちなみに、Go言語のバージョン管理で goenv というのもあるらしい。まあ必要になったら入れようと思います。
Goglandのインストールと設定
GoglandはJetBrains社が2016年に出したGo言語用のIDEですね。今のところ無料で使えるので使ってます。
インストール後は、GOPATHを設定するだけでもう開発できちゃいます。
※ 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の"Download The SDK"に従ってとってくるだけです。
ただ、上にも書いたように今回はappcfg.py
を利用するため、 Google Cloud SDKだけじゃなく、Optional: Download and install the original App Engine SDK for Goの方もダウンロード&インストールします(両方必要なのかどうかは不明)。
direnvのインストール
direnvとは、対象のディレクトリをカレントディレクトリにした際に環境変数を自動で書き換えてくれる君です。Go言語の場合、GOPATHの環境変数を設定しないと動かなかったりするので、この子を使うととても便利っぽい。
インストールは、Macの場合以下だけでできます(Homebrewを入れてるの前提)。
$ brew install direnv
インストール後、.bash_profile
に以下の設定をします。
# エディタはお好きなものでOK
export EDITOR=/usr/bin/vim
eval "$(direnv hook bash)"
また、/ProjectDirで以下のコマンドを打ち、.envrcファイルを作成します。
$ cd ${/ProjectDir}
$ direnv edit .
# 上記設定の場合Vimが立ち上がるので、以下入力
export GOPATH=$(pwd)
glideのインストール
glideはGo言語用の各プロジェクトごとのパッケージ管理らしいです。
プロジェクトごとの、とあえていうのは、Go言語には言語仕様でパッケージ管理っぽいことができるらしい(go getのことなのかな? まだよくわかってない。。。)のですが、その場合globalとなるため、色々アレだよねっていうのでできたらしいです。
インストールは以下できます。そう、Macならね。
$ brew install glide
まだインストールするだけで、特に何も設定はしません。
サンプルプロジェクトのダウンロードと動作確認
本当はglideとかdirenvとかは入れなくてもサンプルプロジェクトは動くのですが。
サンプルプロジェクトと動かし方は Googleさんのクイックスタートに書いてあるので、ここでは割愛します。
(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ファイルがないとエラーになるっぽいです。
依存関係のダウンロードは以下のコマンドを打つだけです。
$ 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だけを作成し、そこに全部突っ込みます。
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を利用し、以下のようにします。
$ cd ${/ProjectDir}
$ dev_appserver.py app.yaml
動作確認してみます。
$ 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のインタフェースって癖があると感じちゃうというか、なんというか。
進捗ダメです。