Google App Engine for GoがGo1.8に対応したので試してみた #golang #gcp
June 30, 2017
この記事はQiitaの記事をエクスポートしたものです。内容が古くなっている可能性があります。
はじめに
Go Conference 2017 Springのときに、そろそろリリースされるという噂が流れたGoogle App Engine Standard Edition (GAE SE)のGo1.8対応ですが、2017年6月 27日についに対応しました(まだ、ベータですが)。
https://cloud.google.com/appengine/docs/standard/go/release-notes
この記事では、GAE SEのGo1.8への移行方法とGo1.8になると何が嬉しいのか、移行する際に問題となる点についてまとめたいと思います。
SDKのアップデートと設定の変更
まずはApp Engine SDKをアップデートしましょう。 gcloud
コマンドを使ってる方は、以下の通りgcloud components update
をかけましょう。
$ gcloud components update
6/30追記: まだgcloudの方にはきてないそうです https://groups.google.com/d/msg/google-appengine-go/sOg4eaEpst0/ujFIJSOoAAAJ
App Engine SDKを直に使っている方は、ダウンロードページから落としてきましょう。
私は、App Engine SDKを直に使っているので、落としてきたディレクトリをls
をしてみると以下のようになりました。
ls ~/Documents/go_appengine
BUGS VERSION bulkload_client.py download_appstats.py godoc goroot php_cli.py wrapper_util.pyc
LICENSE _python_runtime.py bulkloader.py endpointscfg.py gofmt goroot-1.6 run_tests.py
RELEASE_NOTES appcfg.py demos go-app-stager google goroot-1.8 tools
RELEASE_NOTES.python backends_conversion.py dev_appserver.py goapp gopath lib wrapper_util.py
goroot-1.6
とgoroot-1.8
というディレクトリがあることが分かります。
前のバージョンでは、goroot
しかなかったので、Go1.6とGo1.8を使う場合で切り替えられるようになっているようです。
さて、goapp
コマンドを使って、実際に使われているGoのバージョンやGOROOT
を見てみましょう。
ちなみに、go
コマンドやgoapp
コマンドにはenv
というサブコマンドがあって、使用される環境変数を確認することができます。
$ goapp version
go version 1.6.4 (appengine-1.9.56) darwin/amd64
$ goapp env GOROOT
/Users/tenntenn/Documents/go_appengine/goroot-1.6
なるほど、デフォルトではgoroot-1.6
の方を使うようですね。
では、goroot-1.8
を使うにはどうすれば良いでしょうか?
リリースノートを見ると、api_version
をapi_version: go1
からapi_version: go1.8
に変更するようにと書かれています。
そこで、app.yaml
の設定をたとえば以下のように変更してみます。
application: my-project-id
module: default
runtime: go
api_version: go1.8
version: main
handlers:
- url: /.*
script: _go_app
secure: always
そして、再度バージョンとGOROOT
を確認してみます。
$ goapp version
go version 1.8.3 (appengine-1.9.56) darwin/amd64
$ goapp env GOROOT
/Users/tenntenn/Documents/go_appengine/goroot-1.8
おぉ。バージョンがGo1.8に変わりましたね。
どうやらapp.yaml
の内容を見て切り替えているようです。
context.Context
の移行と問題点
context.Context
の移行
Go1.7で標準パッケージにcontext
パッケージが追加されました。
それまで使われていた準標準パッケージのgolang.org/x/net/context
パッケージは使われなくなりました。
標準パッケージにcontext
が入ることで、多くの標準パッケージでcontext.Context
が使われるようになりました。
何が加わったのか確認したい方は、Goのリリースノートやリリースパーティーの資料を見るとよいでしょう。
さて、GAE SEにおいてもGo1.8に上がったことで、golang.org/x/net/context
ではなく、context
パッケージを使用することになりました。
そもそも、golang.org/x/net/context
のContext
型はインタフェースであり、Go1.8のcontext
パッケージのContext
型と互換あります。
Goにおいて、インタフェース型の変数への代入はインタフェースで規定しているメソッドを実装しているかどうかで決まります。
そのため、golang.org/x/net/context
パッケージのContext
インタフェースを実装している型は、同時にGo1.8のcontext
パッケージのContext
インタフェースも実装していることになります。
golang.org/x/net/context
パッケージから標準パッケージのcontext
パッケージへの移行は比較的簡単で、基本的にはインポート文を変更するだけで対応できます。
sed
などで置換してもいいですが、Go1.8ではgo tool fix
で置換することが可能です。
以下のように実行すると、main.go
でインポートしているgolang.org/x/net/context
をcontext
に変更することができます。
$ go tool fix -force=context main.go
なお、goapp
を使っていないのは、goapp
のtool
コマンドにfix
が無いためです。
移行の際の問題点
golang.org/x/net/context
パッケージから標準パッケージのcontext
パッケージへの移行は、Context
インタフェースに互換性があるため、比較的簡単であると述べました。
基本的には、インポート文を変更するだけで問題なく、たとえ標準パッケージのcontext.Context
をライブラリなどが提供するgolang.org/x/net/context.Context
を取る関数などに引数として渡しても問題ありません。
しかし、いくつか例外があります。
たとえば、以下のように引数にクロージャを取るような関数などがライブラリで提供されている場合、クロージャの型自体は互換がないため渡すことができません。
func DoSomething(f func(c context.Context) {// contextはgolang.org/x/net/contextだとする
}
また同様にスライスの要素やマップのキーやバリューなどコンポーネント型で使用している場合には互換がありません。
var slice []context.Context
var m map[*http.Request]context.Context
これらはすべて、引数や要素の型を含めて型として定義されているためで、たとえメソッドが同じであるインタフェースを引数や要素に取っていても、コンポーネント型や関数型の型は異なるためです。
このような問題に対応するには、ライブラリ側がビルドタグでGo1.6とGo1.7以降で使用するcontext.Context
型を切り替えてやる必要があります。
もしご自身でライブラリを作られている場合は、osamingo/jsonrpc
の対応などを参考にすると良いでしょう。
OSSのライブラリがビルドタグで切り替えていない場合には、PRを送るかラップしてその場をしのぐ必要があります。
context.Context
の問題はこれだけではありません。
ライブラリの中で*http.Request
を元にcontext.Context
を生成するようなライブラリでは多くの場合問題が発生します。
たとえば、以下のようにgorilla/mux
ではうまくappengine.NewContext
でリクエストからコンテキストを生成することができません。
Go 1.8 Beta - unable to use gorilla/mux - “NewContext passed an unknown http.Request” - Google グループ
package hello
import (
"io"
"net/http"
"github.com/gorilla/mux"
"google.golang.org/appengine"
"google.golang.org/appengine/log"
)
func init() {
mx := mux.NewRouter()
mx.HandleFunc("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := appengine.NewContext(r)
log.Debugf(ctx, "time to say hello!")
io.WriteString(w, "howdy!")
}))
http.Handle("/", mx)
}
また、同様にlionのv2でもうまくcontext.Context
を取得することができません。
これはGAEのコンテキストがリクエストと対になって管理されていることが原因です。 https://github.com/golang/appengine/blob/eda0abe86b8018c6924fac5f669c9f52eb0c68b8/internal/api.go#L198-L208
var ctxs = struct {
sync.Mutex
m map[*http.Request]*context
bg *context // background context, lazily initialized
// dec is used by tests to decorate the netcontext.Context returned
// for a given request. This allows tests to add overrides (such as
// WithAppIDOverride) to the context. The map is nil outside tests.
dec map[*http.Request]func(netcontext.Context) netcontext.Context
}{
m: make(map[*http.Request]*context),
}
GAEではappengine.NewContext
をすると、引数で渡したリクエストに対応するコンテキストを取得します。
その際、以下のようにライブラリの中でRequest.WithContextを用いていると、ハンドラにくる*http.Request
を上書いてしまい、コンテキストが取得できません。
https://github.com/gorilla/mux/blob/0a192a193177452756c362c20087ddafcf6829c4/context_native.go#L19
func contextSet(r *http.Request, key, val interface{}) *http.Request {
if val == nil {
return r
}
return r.WithContext(context.WithValue(r.Context(), key, val))
}
この問題はissueになっているのでそのうち対応がされるとは思いますが、結構難しい問題ではないかと考えています。
Goのnet/http
パッケージを修正して、Request.WithContext
の挙動を変え、*http.Request
をコピーしないという手もありますが、Go側を変えるのは現実的では無いでしょう。
おそらく、GAE側のコンテキストの扱い方を変えるか、Request.WithContext
をラップした関数を作り、その内部で上述したマップを書き換えるのが無難ではないかと考えています。
Go 1.8にアップデートする利点
Go1.6からGo1.8にアップデートするにあたって、何が嬉しいのかざっくり箇条書きにしたいと思います。 詳しい話は、Goのリリースノートやリリースパーティーの資料(1.7、1.8)を読むと良いでしょう。
context
パッケージが標準に- 標準パッケージが多く
context
を使うようになった
- 標準パッケージが多く
- サブテストができるようになった
- sort.Sliceが導入され、型をつくらずにソートできるようになった
ちなみに、Go1.8からplugin
パッケージが入ったのですが、GAE SEのコードでインポートしてもビルドできます。しかし、cgoを使えない環境ではstubが呼び出されるため、常にplugin.Open
はエラーとなります。
おわりに
この記事では、GAE SEのGo1.8への移行方法と移行の際に問題になりそうな点を挙げました。 まだベータ版ということなので、これから修正が入るとは思いますが、随時アップデートは見ておこうと思います。
また何かアップデートがあったら追記したいと思います。
追記
Contextの件は、Version: 1.9.57 - 2017-08-07にて修正されました
Go1.8用にgo_appengine/goroot-1.8/src/appengine_internal/api_go18.go
というファイルが追加されました。
このファイル内で、*http.Request
経由で取得できるContext
からGAE用のcontext
を取得しています。
これにより、たとえRequest.WithContextで*http.Request
を上書きしたとしても、リクエストが保持しているコンテキストはコピーされ、そこからGAEのコンテキストが取得できるというわけです。
コンテキストにコンテキストを入れるという大胆な方法になったということですね!