Google App Engine for GoがGo1.8に対応したので試してみた #golang #gcp

## はじめに

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を直に使っている方は、[ダウンロードページ](https://cloud.google.com/appengine/docs/standard/go/download)から落としてきましょう。

私は、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`を使うにはどうすれば良いでしょうか？

[リリースノート](https://cloud.google.com/appengine/docs/standard/go/release-notes)を見ると、`api_version`を`api_version: go1`から`api_version: go1.8`に変更するようにと書かれています。
そこで、`app.yaml`の設定をたとえば以下のように変更してみます。

```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のリリースノート](https://golang.org/doc/devel/release.html)や[リリースパーティーの資料](http://go-talks.appspot.com/github.com/tcnksm/talks/2017/02/go1.8/go1.8.slide#38)を見るとよいでしょう。

さて、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`を取る関数などに引数として渡しても問題ありません。
しかし、いくつか例外があります。

たとえば、以下のように引数にクロージャを取るような関数などがライブラリで提供されている場合、クロージャの型自体は互換がないため渡すことができません。

```go
func DoSomething(f func(c context.Context) {// contextはgolang.org/x/net/contextだとする
}
```

また同様にスライスの要素やマップのキーやバリューなどコンポーネント型で使用している場合には互換がありません。

```go
var slice []context.Context
var m map[*http.Request]context.Context
```

これらはすべて、引数や要素の型を含めて型として定義されているためで、たとえメソッドが同じであるインタフェースを引数や要素に取っていても、コンポーネント型や関数型の型は異なるためです。
このような問題に対応するには、ライブラリ側がビルドタグでGo1.6とGo1.7以降で使用する`context.Context`型を切り替えてやる必要があります。
もしご自身でライブラリを作られている場合は、[`osamingo/jsonrpc`](https://github.com/osamingo/jsonrpc/blob/master/context17.go)の対応などを参考にすると良いでしょう。
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 グループ](https://groups.google.com/forum/#!topic/google-appengine-go/Av7Lg956D6Y)

```go
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](http://gopkg.in/celrenheit/lion.v2)でもうまく`context.Context`を取得することができません。

これはGAEのコンテキストがリクエストと対になって管理されていることが原因です。
https://github.com/golang/appengine/blob/eda0abe86b8018c6924fac5f669c9f52eb0c68b8/internal/api.go#L198-L208

```go
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](https://golang.org/pkg/net/http/#Request.WithContext)を用いていると、ハンドラにくる`*http.Request`を上書いてしまい、コンテキストが取得できません。

https://github.com/gorilla/mux/blob/0a192a193177452756c362c20087ddafcf6829c4/context_native.go#L19

```go
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](https://github.com/golang/appengine/issues/66)になっているので[そのうち対応がされる](https://groups.google.com/d/msg/google-appengine-go/Av7Lg956D6Y/lN5w1ef8AAAJ)とは思いますが、結構難しい問題ではないかと考えています。
Goの`net/http`パッケージを修正して、`Request.WithContext`の挙動を変え、`*http.Request`をコピーしないという手もありますが、Go側を変えるのは現実的では無いでしょう。
おそらく、GAE側のコンテキストの扱い方を変えるか、`Request.WithContext`をラップした関数を作り、その内部で[上述したマップ](https://github.com/golang/appengine/blob/eda0abe86b8018c6924fac5f669c9f52eb0c68b8/internal/api.go#L198-L208)を書き換えるのが無難ではないかと考えています。

## Go 1.8にアップデートする利点

Go1.6からGo1.8にアップデートするにあたって、何が嬉しいのかざっくり箇条書きにしたいと思います。
詳しい話は、[Goのリリースノート](https://golang.org/doc/devel/release.html)やリリースパーティーの資料（[1.7](http://go-talks.appspot.com/github.com/tcnksm/talks/2016/08/go1.7/go1.7.slide)、[1.8](http://go-talks.appspot.com/github.com/tcnksm/talks/2017/02/go1.8/go1.8.slide))を読むと良いでしょう。

* `context`パッケージが標準に
    * 標準パッケージが多く`context`を使うようになった
* サブテストができるようになった
* [sort.Slice](https://golang.org/pkg/sort/#Slice)が導入され、型をつくらずにソートできるようになった

ちなみに、Go1.8から`plugin`パッケージが入ったのですが、GAE SEのコードでインポートしてもビルドできます。しかし、cgoを使えない環境では[stub](https://github.com/golang/go/blob/master/src/plugin/plugin_stubs.go)が呼び出されるため、常に`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](https://golang.org/pkg/net/http/#Request.WithContext)で`*http.Request`を上書きしたとしても、リクエストが保持しているコンテキストはコピーされ、そこからGAEのコンテキストが取得できるというわけです。

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