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.6goroot-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_versionapi_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/contextContext型はインタフェースであり、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/contextcontextに変更することができます。

$ go tool fix -force=context main.go

なお、goappを使っていないのは、goapptoolコマンドに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.71.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のコンテキストが取得できるというわけです。

コンテキストにコンテキストを入れるという大胆な方法になったということですね!