Google App EngineでGoのバージョンアップを行う #golang #gae

## はじめに

メルペイ エキスパートチームの@tenntennです。
この記事は技術書典4で頒布した["こうして僕らは、書籍を売るアプリを作った 2.0.1"](https://techbookfest.org/event/tbf04/circle/14710011)の一部を少し改定したものです。

Google App Engine for Go （GAE/Go）で開発する上で付き合っていかないといけない問題として、GAEのSDKのバージョンやそこで使用されるGo自体のバージョンアップが挙げられます。
GAE/Goはクラウドサービスであるため、SDKや言語のバージョンアップは必ず追従して行く必要のある問題です。
バージョンアップしなくても、一定期間はそのまま使えることが多いでしょう。
しかし、場合によっては非推奨になり、その後使えなくなる可能性もあり得ます。
そのため、特に大きな問題がない場合は積極的にバージョンアップをしていくとよいでしょう。

Goのバージョンを上げることによって、次のような恩恵を受けることが期待できます。

 * 使える機能やパッケージが増える
 * ランタイムの改善によりパフォーマンスが上がる
 * ツール群の改善により開発効率が上がる

加えて、Goはバージョン1の間は言語による[後方互換性](https://golang.org/doc/go1compat)が担保されているため、言語自体の後方互換性は心配する必要はありません。

一方で、言語以外の部分で次のような問題が生じる可能性があります。

 * 使用しているライブラリの互換性
 * [golang.org/x](https://godoc.org/-/subrepo)以下で管理されている準標準パッケージが標準パッケージに昇格した場合の扱い

この記事では、このようなGAE/Goを用いた開発の中における、Goのバージョンアップにまつわる話をまとめてみようと思います。

## Google App EngineのGoのバージョンの怖い話

### Google App EngineのGoのバージョン指定

Goのバージョンは現在半年に一度のサイクルでアップデートされています。
Google App EngineもGo1.6まではそのバージョンアップサイクルに多少おくれながらも追いついてきていました。
しかし、Go1.7で標準パッケージに導入されたコンテキストの互換性の問題により、Go1.7以降はなかなかGoogle App EngineにはGoのバージョンアップがやってきませんでした。

Go1.8がリリースされ、Google Cloud PlatformのユーザコミュニティではGoogle App EngineのGo1.8の追従が期待されていました。そしてついに、Go1.8にアップデートされることが決定され、無事リリースに至りました。

Go1.8対応がリリースされた際にGoogle App Engineのアプリケーションの設定を行う`app.yaml`ファイルの設定方法にもアップデートがありました。

それまでの`app.yaml`ファイルでは、以下のように`api_version`には単に`go1`とだけ書いていました。
これはGo1の間は後方互換が保たれているので、マイナーバージョンの指定は不要という理由からだと推測できます。

```yaml
api_version: go1
```

一方、新しい指定の方法では、以下のように、マイナーバージョンまで指定することができるようになりました。

```yaml
api_version: go1.8
```

これにより、Goのバージョンを指定してランタイムを切り替えることが可能になりました。

### バージョンを詳細に指定すべき理由

新しいGoogle App EngineのSDKからは、`app.yaml`ファイルの`api_version`にマイナーバージョンを指定することが可能になりました。
しかし、後方互換のためにマイナーバージョンを指定しなくても`api_version: go1`のように指定してもGoogle App EngineのSDKで定められたデフォルトのバージョンが使われるようになっています。

この変更がリリースされた当初では、マイナーバージョンを指定しない場合には、Go1.6としてデプロイされていました。しかし、その後のバージョンアップでデフォルトがGo1.8に変更になったのです。

そのため、プロダクトを開発する上でマイナーバージョンを指定せずにデプロイしていると、ある日突然意図しないマイナーバージョンでアプリケーションが動作することが考えられます。

なぜGoのランタイムのバージョンが突然変わると問題なのでしょうか。
バージョンとbuild constraintsにまつわる、現実に起きうる話をしていきましょう。

### バージョンとbuild constraintsにまつわる怖い話

[build constraints](https://golang.org/pkg/go/build/#hdr-Build_Constraints)とは、ビルドタグとして知られるOSやアーキテクチャ、Goのバージョンによってビルド対象のファイルを切り替えるためのしくみです。
例えば、ファイルの先頭に以下のようにバージョンを指定して書くと、そのバージョン以降のGoのコンパイラでしかビルド対象になりません。つまり以下の例では、Go1.7以上でしかビルドされません。

```go
// +build go1.7
```

build constraintsは、非常に便利で多くのライブラリなどで活用されています。
しかし、`app.yaml`ファイルでマイナーバージョンを指定していない場合に非常に厄介な問題に直面します。

`api_version`に`go1`を指定していた場合、デフォルトのバージョンのGoのコンパイラでビルドされてしまいます。

そして、使用しているライブラリ内でbuild constraintsでビルドをGoのバージョンによって分けている場合に問題が生じてしまう場合があります。

例えば、以下の２つのコードについて考えてみます。
上のコードでは、build constraintsによってGo1.6以下のコンパイラでしかビルドできないようになっています。一方で、下のコードではGo1.7以上でコンパイルされるように指定されています。
どちらのファイルにも`Hello`関数が定義されており、Goのバージョンによって挙動が異なっています。
これまでGo1.6を使ってビルドされていた場合に、ある日突然Go1.8でデプロイされたとすると、それ以降`Hello`関数が`panic`を起こし始めます。
ビルド時には特にコンパイルエラーになる訳ではないため、デプロイして実際に動かすまで異常に気がつくことはできません。

```go
// +build !go1.7

package hello

func Hello() string {
    return "Hello"
}
```

```go
// +build go1.7

package hello

func Hello() string {
    panic("error")
    return ""
}
```

ここで見た例は非常に極端ですが、似たような問題が生じることは十分に考えられます。
実際に、一部のWebフレームワークでコンテキストの取り回しをGoのバージョンによって分けている事例がありました。
安全にライブラリを導入するには、導入の際にbuild constraintsをしているか調べ、現在使用しているバージョン以外でどうのような挙動をするかを十分に検証することです。
しかし、現実的には最低限ランタイムのバージョンを上げる際に検証することをオススメします。
またそれ以前に、`api_version`の指定は`go1.8`のように、マイナーバージョンも含めてバージョン指定を行うようにするとよいでしょう。

## コンテキストの歴史にまつわるインタフェースの互換性と型エイリアス

### コンテキストの遍歴とGoのバージョンアップ

Google App Engine上のアプリケーションからGoogle Cloud Platformが提供する機能を用いるには、コンテキストというオブジェクトが必要になります。
コンテキストは多くの場合に関数やメソッドの第1引数に取ります。
例えば、以下コードはCloud Datastoreからデータを取得する`Get`関数を呼び出しています。そして、第1引数の`ctx`はコンテキストです。

```go
var data Data
if err := datastore.Get(ctx, key, &data); err != nil {
    // エラー処理
}
```

このようにGoogle App Engineでは、コンテキストを多用するのですが、歴史的経緯から今まで次の3種類のコンテキストが存在しました。

 * `appengine.Context`
 * `golang.org/x/net/context.Context`
 * `context.Context`

`appengine.Context`は最も古いコンテキストです。
 コンテキストという概念がGoの標準に入る前から存在しています。

`golang.org/x/net/context.Context`はGo1.6まで利用されていた準標準パッケージとして提供されていたコンテキストです。Google App EngineのランタイムがGo1.8へバージョンアップされる段階で`context.Context`が利用されるようになりました。

`context.Context`はGo1.7以降に標準パッケージに導入されたコンテキストで、中身はGo1.6以前の`golang.org/x/net/context.Context`と同じものです。

このため、ランタイムがGo1.6からGo1.8に変わる際に、コンテキストの互換性について議論がされるようになりました。そして、Go1.9へアップデートされる際に型エイリアスの登場によって、さらに状況が変わってきました。ここでは、Go1.6からGo1.8、そしてGo1.9にアップデートされていく中で、コンテキストの互換性がどのように変化していったのかという点について説明していきます。そして、その中でインタフェースの互換性と型エイリアスの存在意義について議論していきます。

### Goのインタフェース

Goのインタフェースについて詳しい解説は["インタフェースの実装パターン"](https://qiita.com/tenntenn/items/eac962a49c56b2b15ee8)という記事に書いていますのでそちらを参照して下さい。ここでは簡単にインタフェースの解説を行います。

インタフェースは、メソッドのリストです。ある型がインタフェースを実装するということは、その型がインタフェースで定義しているメソッドをすべて持っているという状態を指します。

例えば、以下ようなインタフェースを考えてみます。

```go
type InterfaceA interface {
  M() string
}
```

`InterfaceA`は`M() string`というメソッドを持ちます。
つまり、このインタフェースを実装するには、以下のようにメソッド`M`を持つ型を定義してやれば良いでしょう。

```go
type Str string

func (s Str) M() string {
  return string(s)
}
```

どのような型でも構いませんが、とにかくメソッド`M`を同じシグニチャで定義していれば問題ありません。
このとき、`Str`型の定義には、`implements`のように明示的に`InterfaceA`を実装することを表す記述は必要ありません。単にメソッドを用意するだけで、インタフェースを実装したことになります。

さて、このGoのインタフェースの特徴を踏まえてインタフェースと型エイリアスの議論を進めていきましょう。

### インタフェース同士の互換

以下のような、同じメソッドを持つ2つのインタフェースを考えてみましょう。

```go
type InterfaceA interface {
  M() string
}

type InterfaceB interface {
  M() string
}
```

`InterfaceA`と`InterfaceB`は同じシグニチャのメソッドを持ちます。
そのため、`InterfaceA`を実装していれば、自ずと`InterfaceB`も実装していることになります。

つまり、以下のよう明示的に型変換を行わずに、それぞれの型の変数を代入しあうことができます。

```go
type InterfaceA interface {
  M() string
}

type InterfaceB interface {
  M() string
}

var a InterfaceA
var b InterfaceB

a, b = b, a
```

ここで最初に出てきたコンテキストの話題に戻りましょう。
`golang.org/x/net/context.Context`型はGo1.8までは以下のように[定義](https://github.com/golang/net/blob/master/context/pre_go19.go#L15)されています。

```go
type Context interface {
  Deadline() (deadline time.Time, ok bool)
  Done() <-chan struct{}
  Err() error
  Value(key interface{}) interface{}
}
```

一方、Go1.7から提供されている標準パッケージの`context.Context`型は以下のように[定義](https://golang.org/pkg/context/#Context)されています。

```go
type Context interface {
  Deadline() (deadline time.Time, ok bool)
  Done() <-chan struct{}
  Err() error
  Value(key interface{}) interface{}
}
```

各インタフェースのメソッドを見ると、すべてのメソッドのシグニチャが一致していることが分かります。
つまり、`golang.org/x/net/context.Context`を実装する型は標準パッケージの`context.Context`も実装していることになります。
そのため、引数で`golang.org/x/net/context.Context`型として渡されてきた値は、標準パッケージの`context.Contextx`を引数に取る関数に、特になにもせずそのまま渡すことができます。

このようなGoのインタフェースの特徴からGoogle App Engineにおいても、Go1.6からGo1.8に移行する際に、大きな問題が起きずに`import`を`golang.org/x/net/context`から`context`に変更するだけで大部分は標準パッケージの`context.Context`に移行できました。

しかし、いくつか移行の際に問題が生じる場合がありました。

例えば、以下のようなインタフェースを引数として取るような関数を考えてみます。

```go
type InterfaceA interface {
  M() string
}

func callM(a InterfaceA) {
  a.M()
}
```

関数`callM`は引数として`InterfaceA`型の値を取ります。
さて、ここで以下のように関数`callM`を変数にいれることを考えてみましょう。

```go
var f func(InterfaceA) = callM
```

次に、以下のように`InterfaceB`型の値を引数として取る関数を考えてみましょう。

```go
type InterfaceB interface {
  M() string
}

func callM2(b InterfaceB) {
  b.M()
}
```

さて、関数`callM2`は以下のように`func(InterfaceA)`型の変数`f`に代入できるでしょうか？

```go
var f func(InterfaceA) = callM2
```

答えはできません。
変数`f`の型は`func(InterfaceA)`です。
引数の型が`InterfaceA`である点が`callM2`と異なっています。
たとえ`InterfaceA`と`InterfaceB`が持つメソッドリストが同じであっても、型としては別ものであるため、代入ができません。

そのため、`golang.org/x/net/context.Context`を引数に取る関数と`context.Context`を引数にとる関数は別の型という扱いになってしまいます。
例えば、Cloud Datastoreを扱うためのパッケージである`google.golang.org/appengine/datastore`パッケージの[`RunInTransaction`](https://godoc.org/google.golang.org/appengine/datastore#RunInTransaction)では引数に`func(context.Context) error`型の関数を取ります。

この関数の第1引数のコンテキストは、執筆時は標準パッケージの`context.Context`ではなく、`golang.org/x/net/context.Context`です。

例えば、以下のようなコードを書いていたとします。
コンテキストは、Go1.6まで使用されていた準標準パッケージの`golang.org/x/net/context.Context`を用いています。

```go
import "golang.org/x/net/context"
// ...略

func doSomething(ctx context.Context) {
  // ...略
  err := datastore.RunInTransaction(ctx, func(ctx context.Context) error {
    // 何か処理
  }, nil)
  // ...略
}
```

標準パッケージの`context.Context`を用いるように修正することを考えます。
`import`の部分を以下のように書き換えればようさそうです。

```go
import "context"
// ...略

func doSomething(ctx context.Context) {
  // ...略
  err := datastore.RunInTransaction(ctx, func(ctx context.Context) error {
    // 何か処理
  }, nil)
  // ...略
}
```

しかし、このままだとコンパイルエラーが発生してしまいます。
`datastore.RunInTransaction`関数の第1引数のコンテキストの型は`golang.org/x/net/context.Context`型なので、標準パッケージの`context.Context`型の値を渡してもコンパイルエラーにはなりません。
一方で、第2引数に取る関数の型は`func(context.Context) error`です。
この関数が引数に取っているコンテキストは、`golang.org/x/net/context.Context`型です。
そのため、標準パッケージの`context.Context`を引数に取る関数を渡してしまうと型が異なるため、コンパイルエラーになってしまいます。

コンパイルエラーを避けるためには、以下のように関数をラップして型を変換する必要があります。

```go
import "context"
import netctx "golang.org/x/net/context"
// ...略

func wrap(f func(ctx context.Context) error) func(netctx.Context) error {
  return func(nctx netctx.Context) error {
    return f(ctx)
  }
}

func doSomething(ctx context.Context) {
  // ...略
  err := datastore.RunInTransaction(ctx, wrap(func(ctx context.Context) error {
    // 何か処理
  }), nil)
  // ...略
}
```

このように、同じメソッドリストを持つインタフェース同士は、一見、互換性があるように見えます。
しかし、実際には型が異なるため完全な互換性というものはありません。
そのため、Go1.8までは準標準パッケージの`golang.org/x/net/context.Context`型から標準パッケージの`context.Context`型に移行するためには一筋縄ではいきませんでした。
このようなリファクタリングの問題を解決するために、Go1.9では型エイリアスという機能が導入されました。
次に型エイリアスとそれがGoogle App Engineにおいて、どのような恩恵を与えるかを解説します。

### 型エイリアスとGoogle App Engineにおける恩恵

型エイリアスはGo1.9で導入された概念です。
以下のような定義があった場合、`Bar`型は`Foo`型の型エイリアスになります。

```go
type Foo struct {
  S string
}

type Bar = Foo
```

`Bar`型は、`Foo`型を基にした新しい型ではなく、完全に同じ型です。
型エイリアスで定義した型には、新しくメソッドを追加することはできません。

例えば、以下のように、`int`型を基にした型と`int`型のエイリアス型の名前を表示してみましょう。
なお、型名の表示するためには、`fmt.Printf`関数で書式を`%T`を指定しています。

```go
type N int
type M = int
var n N = 10
var m M = 10
// main.N, int
fmt.Printf("%T, %T\n", n, m)
```

このコードを実行すると、コメントで書いているように、`main.N, int`と表示されます。
変数`n`の型名は、`main.N`、変数`m`は`int`となります。
変数`n`の型は、`int`型を基にして新しい型として定義した`N`型であるため、その型名が表示されています。
一方で`M`型は、`int`型のエイリアス型であるため、`int`と表示されています。

このように、型エイリアスは既存の型に新しく名前をつけ、同じ型として振る舞ってほしい場合に用います。
さて、この型エイリアスは実際の開発の現場で、どのように使うものなのでしょうか？
GoチームのRuss Cox氏の書いた["Codebase Refactoring (with help from Go)"](https://talks.golang.org/2016/refactor.article)という記事によると、型エイリアスはリファクタリングのために開発されたと述べられています。

ここで、Google App Engineにおけるコンテキストの遍歴を思い出してみましょう。
コンテキストの型は次のように移り変わってきました。

 * `appengine.Context`
 * `golang.org/x/net/context.Context`
 * `context.Context`

そして、準標準パッケージの`golang.org/x/net/context.Context`型から標準パッケージの`context.Context`型に移り変わる際には、前述したようなインタフェースの互換性が問題になる場合があると述べました。

この問題を解決するために、Go1.9の`golang.org/x/net/context.Context`型の[実装](https://github.com/golang/net/blob/release-branch.go1.9/context/go19.go)は、以下のように標準パッケージの`context.Context`型エイリアスになっています。

```go
type Context = context.Context
```

型エイリアスとして定義することで、型の互換性の問題は完全に解決します。
Go1.9以降では、`golang.org/x/net/context.Context`型は標準パッケージの`context.Context`と完全に一致します。
そのため、`golang.org/x/net/context`パッケージの代わりに、標準の`context`パッケージをインポートするだけで移行が完了します。

執筆時（2018年4月）の最新版のGoogle App EngineのSDKでは、Go1.9がベータ版として利用できるようになっています。
`app.yaml`ファイルの`api_version`に`go1.9`を指定すれば利用できます。

Go1.9がGoogle App Engineで利用できるようになった際には、ぜひコンテキストの移行をしてみてください。

## おわりに

この記事では、Google App EngineのGoのバージョンアップにまつわる話をGoのbuild constraintsや型エイリアスなどの言語機能の側面から議論しました。

ここで述べた事が読者の皆様がご自身の環境でバージョンアップする際にお役に立てば幸いです。