一个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
文章的內容 de
我是一個使用Go語言不久的初學者,我接到了在這個環境下進行開發的工作,但是由於在中途加入,所以一直都不太懂在做什麼。不過,只是這樣並不滿足,為了學習的目的,我決定從頭開始構建。
我將使用Echo的v2作為Web應用程序的框架,同時使用goon來方便操作Google Cloud Datastore進行數據操作。
※我同时也将这作为我的备忘录,所以可能有一些冗杂的描述,请您谅解。
做过的事情
-
- (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版本上运行的(在安装后才注意到),所以我觉得最好保持一致,但目前还没有遇到任何问题,所以就保持现状。
顺便一提,据说还有一个叫做goenv的Go语言版本管理工具。需要时我会考虑安装它。
Gogland的安装和设置。
Gogland是JetBrains在2016年推出的专为Go语言设计的集成开发环境(IDE)。我现在可以免费使用它。安装后,只需设置好GOPATH,就可以开始开发了。

Go语言的App Engine SDK
这是用于将使用Go语言创建的项目部署到App Engine的SDK。此外,还有一些工具可以在开发环境中模拟AppEngine环境(如Memcache和Datastore)。
以前,据说使用SDK内的appcfg.py脚本进行部署,但最近已经整合到gcloud命令中。因此,我认为appcfg.py可能已经被弃用了,但是经过尝试,使用gcloud时,无法成功部署使用Echo+goon+glide创建的项目,所以我将继续使用以前的方式进行构建(如果有人知道使用gcloud进行部署的方法,请告诉我> <)。
これも簡単ですね。 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的快速入门指南,此处不再赘述。(只需下载Hello World应用程序和测试应用程序即可运行)
Echo v2.0とGoonでHello Worldを作成する
一旦建立了开发环境,我将使用Echo和Goon来制作一个简单的Web应用程序。
以下是从这里开始的目录结构。
在以下的目录结构前提下,我们将继续进行对话。
-
- /ProjectDir
.envrc
app.yaml
/src
glide.yaml
glide.lock (glideで作成される)
/backend (ソースコード配置)
/maintest (テストコード配置)
/vendor (glideで作成される)
我并不特别涉及common文件(只是为了我自己备忘,如果在子模块中分离,似乎在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のインタフェースって癖があると感じちゃうというか、なんというか。
进展不好。