> Source: https://www.ymotongpoo.com/works/goblog-ja/toolchain/


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

[Forward Compatibility and Toolchain Management in Go 1.21](https://go.dev/blog/toolchain) by Russ Cox

Go 1.21では[後方互換性へのコミットメントが拡大](https://go.dev/blog/compat)されたことに加えて、
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.mod` に `require M v1.2.3` を追加すれば、プログラムがMのそれより古い
バージョンに対してコンパイルされないことを保証できます。しかし、私のプログラムが特定のバージョンのGoを
必要とする場合、それを表現する手段はこれまでありませんでした。特に、`go.mod` の `go` 行はそれを
表現していませんでした。

たとえば、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.mod` に `go 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.mod` に `go 1.21.1` と書かれたモジュール内で `go build` のような
`go` コマンドを実行すると、Go 1.21.0の `go` コマンドはGo 1.21.1が必要であることに気づき、それを
ダウンロードして、そのバージョンの `go` コマンドを再度呼び出してビルドを完了させます。`go` コマンドが
こうした別のツールチェインをダウンロードして実行するとき、それらをPATH上にインストールしたり、
現在のインストールを上書きしたりすることはありません。代わりに、それらを
[モジュールが持つセキュリティとプライバシーの利点](https://go.dev/blog/module-mirror-launch)を
すべて受け継ぐ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というさらに新しいツールチェインを使いたい、ということを表しています。

`go` と `toolchain` の要求バージョンは、通常のモジュールの要求と同じように `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ツールチェイン」](https://go.dev/doc/toolchain)を参照してください。

By Russ Cox

