Go 1.21における前方互換性とツールチェイン管理

Forward Compatibility and Toolchain Management in Go 1.21 by Russ Cox

Go 1.21では後方互換性へのコミットメントが拡大されたことに加えて、 Goコードの前方互換性も改善されました。これはつまり、Go 1.21以降ではさらに新しいバージョンのGoを 要求するコードを誤ってコンパイルしてしまわないよう、より注意深く振る舞うようになったということです。 具体的には、go.mod 内の go 行が、これまでのリリースではほとんど強制力のない提案に過ぎなかったのに対し、 必要となる最小のGoツールチェインのバージョンを指定するようになりました。

こうした要求バージョンに追従しやすくするために、Go 1.21ではツールチェイン管理という仕組みも導入されました。 これにより、あるモジュールが要求するモジュールの異なるバージョンを使い分けられるのと同じように、 モジュールごとに異なるGoツールチェインを使えるようになります。Go 1.21をインストールした後は、 Goツールチェインを手動でダウンロードしてインストールする必要は二度となくなります。 go コマンドがそれを代わりにやってくれるのです。

この記事の残りの部分では、これら2つのGo 1.21の変更点について、より詳しく説明します。

前方互換性

前方互換性とは、あるGoツールチェインが、より新しいバージョンのGo向けに書かれたGoコードをビルドしようと したときに何が起きるかということです。私のプログラムがモジュールMに依存していて、M v1.2.3で追加された バグ修正を必要としている場合、go.modrequire M v1.2.3 を追加すれば、プログラムがMのそれより古い バージョンに対してコンパイルされないことを保証できます。しかし、私のプログラムが特定のバージョンのGoを 必要とする場合、それを表現する手段はこれまでありませんでした。特に、go.modgo 行はそれを 表現していませんでした。

たとえば、Go 1.18で追加された新しいジェネリクスを使うコードを書く場合、go.mod ファイルに go 1.18 と書けます。しかし、それだけでは以前のバージョンのGoがそのコードをコンパイルしようとするのを防げず、 次のようなエラーが発生してしまいます。

$ cat go.mod
go 1.18
module example

$ go version
go version go1.17

$ go build
# example
./x.go:2:6: missing function body
./x.go:2:7: syntax error: unexpected [, expecting (
note: module requires Go 1.18
$

この2つのコンパイルエラーは紛らわしいノイズです。本当の問題は go コマンドがヒントとして 表示しているものです。プログラムのコンパイルに失敗したので、go コマンドはバージョンの不一致の 可能性を指摘しているのです。

この例では、ビルドが失敗してくれたのは幸運なケースです。もし私が、Go 1.19のパッチリリースで 修正されたバグに依存しているためGo 1.19以降でしか正しく動作しないコードを書いていて、なおかつ そのコードの中でGo 1.19固有の言語機能やパッケージを一切使っていなかったとしたら、それより前の バージョンのGoはそのコードを何事もなくコンパイルしてしまい、静かに成功してしまいます。

Go 1.21以降、Goツールチェインは go.mod 内の go 行を単なるガイドラインではなくルールとして 扱うようになります。また、この行にはパッチリリースやリリース候補版といった具体的なバージョンも 指定できます。つまり、Go 1.21.0は、go.modgo 1.21.1 と書かれたコードすらビルドできないと 理解しますし、go 1.22.0 のようなずっと先のバージョンが指定されたコードであれば言うまでもありません。

これまで古いバージョンのGoにも新しいコードのコンパイルを試みさせてきた主な理由は、不必要な ビルド失敗を避けるためでした。手元のGoのバージョンがプログラムのビルドには古すぎると言われるのは、 特にそれでも動くかもしれない場合(要求バージョンが不必要に保守的なだけかもしれません)や、 新しいバージョンのGoへの更新がちょっとした手間である場合には、とてもいらだたしいものです。 go 行を要求事項として強制することの影響を小さくするために、Go 1.21ではコア配布物にツールチェイン 管理も追加されました。

ツールチェイン管理

Goモジュールの新しいバージョンが必要になったとき、go コマンドがそれをダウンロードしてくれます。 Go 1.21以降では、より新しいGoツールチェインが必要になったときも、go コマンドがそれをダウンロード してくれるようになりました。この機能はNodeの nvm やRustの rustup に似ていますが、別のツールと してではなく、コアの go コマンドに組み込まれています。

Go 1.21.0を使っている状態で、go.modgo 1.21.1 と書かれたモジュール内で go build のような go コマンドを実行すると、Go 1.21.0の go コマンドはGo 1.21.1が必要であることに気づき、それを ダウンロードして、そのバージョンの go コマンドを再度呼び出してビルドを完了させます。go コマンドが こうした別のツールチェインをダウンロードして実行するとき、それらをPATH上にインストールしたり、 現在のインストールを上書きしたりすることはありません。代わりに、それらを モジュールが持つセキュリティとプライバシーの利点を すべて受け継ぐGoモジュールとしてダウンロードし、モジュールキャッシュから実行します。

また、go.mod には新しく toolchain 行も追加されました。これは、特定のモジュール内で作業する際に 使う最小のGoツールチェインを指定するものです。go 行とは対照的に、toolchain は他のモジュールに 対して要求を課すことはありません。たとえば、go.mod は次のように書けます。

module m
go 1.21.0
toolchain go1.21.4

これは、m を要求する他のモジュールは少なくともGo 1.21.0を用意する必要があるが、m 自体の作業では、 少なくともGo 1.21.4というさらに新しいツールチェインを使いたい、ということを表しています。

gotoolchain の要求バージョンは、通常のモジュールの要求と同じように go get を使って 更新できます。たとえば、Go 1.21のリリース候補版のいずれかを使っている場合、次を実行することで 特定のモジュール内でGo 1.21.0を使い始められます。

go get go@1.21.0

これによりGo 1.21.0がダウンロードされて実行され、go 行が更新されます。それ以降 go コマンドを 呼び出すたびに go 1.21.0 という行を認識して、自動的にそのバージョンを再度呼び出すように なります。

あるいは、あるモジュールでGo 1.21.0を使い始めたいものの、より古いバージョンのGoを使っている ユーザーとの互換性を維持しやすくするために go 行は古いバージョンのままにしておきたい場合は、 toolchain 行だけを更新できます。

go get toolchain@go1.21.0

特定のモジュール内でどのバージョンのGoが動いているか気になったときの答えは、これまでと 変わりません。go version を実行してください。

GOTOOLCHAIN環境変数を使えば、特定のGoツールチェインのバージョンを使うよう強制できます。 たとえば、Go 1.20.4でコードをテストするには次のようにします。

GOTOOLCHAIN=go1.20.4 go test

最後に、version+auto という形式のGOTOOLCHAINの設定は、デフォルトでは version を 使いつつ、より新しいバージョンへのアップグレードも許可する、という意味になります。Go 1.21.0を インストールしている状態でGo 1.21.1がリリースされたら、デフォルトのGOTOOLCHAINを設定することで システム全体のデフォルトを変更できます。

go env -w GOTOOLCHAIN=go1.21.1+auto

Goツールチェインを手動でダウンロードしてインストールする必要は、これでもう二度とありません。 go コマンドがそれを代わりにやってくれます。

詳しくは「Goツールチェイン」を参照してください。

By Russ Cox