苦しんで覚えたGo Toolchainの挙動を解説する
Goの標準ライブラリ・コンパイラ・アセンブラ・その他ツール(go build, go testとかのgo xxxコマンド)をまとめたものをツールチェーンと呼ぶ。
また、Go 1.21から導入されたツールチェーンの選択制御機能のことも指す(ここではこちらを解説)。
以降、このブログでは区別のためにツール群を指す場合はツールチェーン、ツールチェーンの制御機構をToolchainと表記する。
導入の背景
Toolchain登場以前はgo.modのgoディレクティブの挙動が直感的でなく、go.mod内のgoディレクティブにどのバージョンが書かれていても、ビルド時に使用されるのはローカルマシンにインストールされているGoだった。
# ローカルのバージョンは1.18.0
$ go version
go version go1.18.0
# モジュールのgoディレクティブは1.19.0
$ cat go.mod
module go-mod-sample
go 1.19.0 # goディレクティブ
ローカルのバージョンがgoディレクティブで指定されたバージョンより低いときにgo buildを実行すると、1.18.0で実行されるが、ソースコードに1.19.0の記法が含まれていなければビルドは成功する。
反対にローカルが1.19.0でモジュールのgoディレクティブが1.18.0の場合、1.18.0の言語セマンティクスでビルドされる(同様にソースに1.19.0の記法が出るとエラー)。
つまり、元々のgoディレクティブはそのモジュールで最低限必要なバージョンを表すが、結局はローカルのバージョンでビルドするだけなのでバージョンを強制するものではなかった。
この挙動が分かりづらいということと、プロジェクトごとにgoのバージョンが異なることもあるのでローカルのバージョンに依存されるの辛いよねってことでToolchainが導入された。
Toolchainにより、ツールチェーンのバージョンをプロジェクトによって柔軟に切り替えることができる。
Toolchainの機能の概要
Toolchainは環境変数GOTOOLCHAINに設定された値に応じて以下いずれかの挙動を取る。
- ローカルのGoのバージョンを使用
- 特定のバージョンのGoをローカルから検索して使用
- 特定のバージョンのGoをダウンロードして使用
なお、特定バージョンのGoをダウンロードする場合、すでにダウンロードしたことがあれば$HOME/go/pkg/mod/golang.org
ディレクトリにキャッシュされているのでそちらを使用する。
Toolchainの設定値と挙動
Toolchainを利用するには以下の3つの方法で環境変数GOTOOLCHAINに決められた形式で値を設定する必要がある。
なお、設定は1->2->3の順に適用されるので1が最優先。
- プロセス環境でGOTOOLCHAINを設定する
(実行例. GOTOOLCHAIN=go1.22.2 go version) - go env -w および go env -uで管理されるユーザー環境で設定する
(設定例. go env -w GOTOOLCHAIN=go1.22.2) - ローカルインストールされたGoツールチェーンの$GOROOT/go.envに記述する
次に、記載できる値とそれぞれの挙動を解説する。
GOTOOLCHAIN | 挙動 | 設定例 |
---|---|---|
local | ローカル環境にインストールされているツールチェーンを使用 | GOTOOLCHAIN=local |
<name> | 特定バージョンのツールチェーンを明示的に指定して使用。PATHを探して当該バージョンが無ければダウンロードして実行 | GOTOOLCHAIN=go1.22.1 |
<name>+auto | <name>はデフォルトのツールチェーンバージョンだが、場合によっては異なるバージョンのツールチェーンの実行を許可。PATHを探して必要バージョンが無ければダウンロードして実行 | GOTOOLCHAIN=go1.22.1+auto |
<name>+path | <name>はデフォルトのツールチェーンバージョンだが、場合によっては異なるバージョンのツールチェーンの実行を許可。PATHを探して必要バージョンが無ければ停止 | GOTOOLCHAIN=go1.22.1+path |
auto | local+autoの略記 | GOTOOLCHAIN=auto |
path | path+autoの略記 | GOTOOLCHAIN=path |
localの挙動
ローカル環境にインストールされているバージョンが使用される。
コマンドによってはgo.modのgo, toolchainディレクティブの値が挙動に影響する。
$ GOTOOLCHAIN=local go version
go version go1.23.3 darwin/arm64 # ローカルのバージョン
$ cat go.mod
module go-mod-sample
go 1.23.4 # ローカルのバージョンより要求Goバージョンが高い
toolchain 1.23.5
# go versionは動作したが、go mod tidyは要求バージョンを満たさないのでエラー
# toolchainディレクティブの値は特に影響なし
$ GOTOOLCHAIN=local go mod tidy
go: go.mod requires go >= 1.23.4 (running go 1.23.3; GOTOOLCHAIN=local)
<name>の挙動
<name>に指定したバージョンが使用される。
過去に指定したことがない場合は当該バージョンをダウンロードする。
以後、ダウンロードしたツールチェーンをキャッシュして使用する。
local同様、コマンドによってはgo.modのgo, toolchainディレクティブの値が挙動に影響する。
$ GOTOOLCHAIN=go1.22.2 go version
go version go1.22.2 darwin/arm64 # 指定したバージョン
$ cat go.mod
module go-mod-sample
go 1.23.4 # 指定バージョンより要求Goバージョンが高い
toolchain go1.23.5
# go versionは動作したが、go mod tidyは要求バージョンを満たさないのでエラー
# toolchainディレクティブの値は特に影響なし
$ GOTOOLCHAIN=local go mod tidy
go: go.mod requires go >= 1.23.4 (running go 1.22.2; GOTOOLCHAIN=go1.22.2)
auto, pathの挙動(依存関係の解決を含まないコマンド)
autoとpathが付くと挙動が複雑になるので、ここからはそれらがついている(あるいは短縮系のautoまたはpath)場合の解説をする。
なお、go getやgo mod tidyといった、依存関係を解決する系のコマンドはまた異なる挙動をするのでこの解説の後で改めて解説する。
GOTOOLCHAINにautoまたはpathが含まれる値で設定されている場合、goコマンドは必要に応じて新しいGoバージョンで実行される可能性がある。
具体的には、現在のワークスペースのgo.workファイルか、ワークスペースがない場合はメインモジュールのgo.modファイルにあるtoolchainディレクティブとgoディレクティブを参照し、それらの値との兼ね合いでデフォルト(<name>)が使用されるかどうかが決まる。
つまり、autoまたはpathを使用することはツールチェーンのバージョンの自動切り替えを許可するということに他ならない。
逆にいうと、autoまたはpathを使用しなければ使用ツールチェーンのバージョンは固定した上で実行を試みることができる(ただし、goディレクティブより古いバージョンを指定するとエラー出る)。
実際の設定例から挙動のパターンを見ていく。
- go.workまたはgo.modファイルに
toolchain <tname>
行があり、<tname>に記載されたバージョンが<name>より新しい場合、ツールチェーンは<tname>のバージョンで実行される。
$ cat go.mod
module go-mod-sample
go 1.23.4
toolchain go1.23.5
# autoをつけてないのでGOTOOLCHAINのバージョンで実行される
$ GOTOOLCHAIN=1.21.0 go version
go version go1.21.0 darwin/arm64
# autoをつけたのでgo.modの<tname>のバージョンで実行される
$ GOTOOLCHAIN=1.21.0+auto go version
go version go1.23.5 darwin/arm64
この時、autoであれば必要なツールチェーンをダウンロードするし、pathであればPATHを検索して必要なツールチェーンのバージョンが存在する時のみ実行する。
- <tname>の値が
default
である場合、ツールチェーンは<name>のバージョンで実行される。
$ cat go.mod
module go-mod-sample
go 1.23.4
toolchain default
# GOTOOLCHAIN=<name>で指定したバージョン1.21.0が利用される
# go<version>が利用されるわけではない
$ GOTOOLCHAIN=1.21.0+auto go version
go version go1.21.0 darwin/arm64
- toolchainディレクティブがファイルに無い場合、goディレクティブの<version> が<name>より新しければ、ツールチェーンは<version>で実行される。挙動としては暗黙的に<tname>が<name>と同じバージョンになっている。
$ cat go.mod
module go-mod-sample
go 1.23.4
# toolchainディレクティブが無いのでgoディレクティブのバージョンが<name>と比較され、
# <name>よりgoディレクティブの方が新しいのでそちらで実行される
$ GOTOOLCHAIN=1.21.0+auto go version
go version go1.23.4 darwin/arm64
- <version>と<tname>より<name>の方が新しい場合は<name>で実行される。
$ cat go.mod
module go-mod-sample
go 1.21.1
toolchain go1.22.2
# <name>のバージョンで実行される
$ GOTOOLCHAIN=go1.22.3+auto go version
go version go1.22.3 darwin/arm64
auto, pathの挙動(依存関係の解決を含むコマンド)
下記のようなモジュールの依存関係を解決するコマンドは、依存先のgoディレクティブのバージョンによって使用するツールチェーンのバージョンも変わってくる。
- go get [package]
- go mod tidy
- go work use
- go work sync
- go install [package@version]
- go run [package@version]
インポートしたいモジュールが必要とするGoバージョンが、手元のモジュールのバージョンよりも低いと困るので、そんな時は自動で必要なツールチェーンのバージョンを調べて更新してくれるというわけである。
しかし、ここでまたややこしいのが、この時選択されるツールチェーンはインポートしたいモジュールが必要とするバージョンのツールチェーンになるとは限らないということ。
ツールチェーンの選択の手順として、Toolchainはまず利用可能なツールチェーンのリストを取得する。
-
auto
の場合は使用可能なツールチェーンのリストをダウンロード -
path
の場合は$PATHをスキャンしてツールチェーンを探し、見つかったツールチェーンのリストを作成
このリストを元に、Toolchainは最大3つの候補を見つけ出す。
- 最新の未リリースGoバージョンのリリース候補 (1.N₃rcR₃)
- 最新のリリース済み Goバージョンの最新パッチ (1.N₂.P₂)
- 前のバージョン のGoツールチェーンの最新パッチ (1.N₁.P₁)
そして、最小バージョン選択の原則に従い、リストの中から最もバージョンが古い候補が選択される。
ここの例は公式を紹介させてもらうが、例えばexample.com/widget@v1.2.3
というモジュールがあるとする。
このモジュールは1.24rc1
以上を必要する。
そして、Toolchainは利用可能なツールチェーンを下記のようにリストアップした。
- 最新の未リリースGoバージョンのリリース候補: 1.29rc2
- 最新のリリース済み Goバージョンの最新パッチ: 1.28.3
- 前のバージョン のGoツールチェーンの最新パッチ: 1.27.9
この場合、Toolchainは1.24rc1
に最も近い1.27.9
を選択する。
しかし、もしwidgetが1.28
以上を要求していた場合は、Toolchainは1.28.3
を選択する。
同様に、もしwidgetが1.29
以上を要求していた場合は、Toolchainは1.29.rc2
を選択する。
go.modの自動更新
ここからはgo.modがどのように更新される可能性があるかを見ていく。
そもそもgoディレクティブとtoolchainディレクティブがどのような意味を持つか改めて確認する。
ディレクティブ | 意味 |
---|---|
go | モジュールが与えられたツールチェーンバージョンのセマンティクスを想定して書かれていることを示す。go 1.22.2 となっているなら、ソースコードに1.22.3以降の記法があってはいけないということ。 |
toolchain | モジュールで使うための推奨ツールチェーンバージョン。このバージョンは、goディレクティブのバージョンより低くすることはできない |
ざっくりまとめると、goディレクティブはモジュールあるいはワークスペースにおいて最小限必要なGoのバージョンであり、toolchainディレクティブは推奨されるツールチェーンバージョンである。
auto, pathを使用する場合、ツールチェーンの自動切り替えを許可することになる。
そのため、新しいGoバージョンを要求するモジュールが存在するとgo.modのgoディレクティブは更新され、新しい最小Goバージョンが記録される。
例えば、go-mod-sampleがgo1.22.10を必要とするモジュールをインポートしているとすると以下のようになる。
# go-mod-sampleはgo1.22.10を必要とするモジュールを使用している
$ cat go.mod
module go-mod-sample
go 1.21.1
toolchain 1.22.0
# <name>+autoで1.22.10をダウンロードしてモジュールをインポートする
$ GOTOOLCHAIN=go1.21.1+auto go mod tidy
# go.modが更新されている
$ cat go.mod
module go-mod-sample
go 1.22.10 # モジュールに最低限必要なバージョン
toolchain go1.23.6 # ツールチェーンのリストから自動選択されたバージョン
require (
~~
)
auto, pathの挙動を疑似コードで表してみるとこういうこと。
if "go.modに元々記載された<version>" >= "インポートモジュールの最大バージョン" {
go_directive = "go.modに元々記載された<version>"
if "go.modに元々記載された<version>" > "go.modに元々記載された<tname>" {
// toolchain_directiveは削除
}
} else if "go.modに元々記載された<version>" < "インポートモジュールの最大バージョン"{
go_directive = "インポートモジュールの最大バージョン"
toolchain_directive = "ツールチェーンのリストから自動選択されたバージョン"
}
ちなみに、auto, pathでなくても、<name>が<tname>より高ければ<tname>は更新される。
例として、ソースコード内でインポートしているモジュールの中にgo1.22.10を必要とするモジュールがあるとする。
しかし、go.modのgoディレクティブとtoolchainディレクティブはどちらも1.22.10未満である。
この時、GOTOOLCHAINに1.22.10以上を指定してやるとgoディレクティブとtoolchainディレクティブが自動更新される。
# go-mod-sampleはgo1.22.10を必要とするモジュールを使用している
$ cat go.mod
module go-mod-sample
go 1.21.1
toolchain 1.22.0
# <name>に1.22.10未満を指定すると実行が失敗する
$ GOTOOLCHAIN=go1.22.2 go mod tidy
~~中略~~
go: toolchain upgrade needed to resolve github.com/livekit/protocol/logger
go: github.com/livekit/protocol@v1.34.0 requires go >= 1.22.10 (running go 1.22.2; GOTOOLCHAIN=go1.22.2)
# <name>に1.22.10より新しいバージョンを指定すると実行が成功する
$ GOTOOLCHAIN=go1.23.0 go mod tidy
# go.modが更新されている
$ cat go.mod
module go-mod-sample
go 1.22.10 # モジュールに最低限必要なバージョン
toolchain go1.23.0 # GOTOOLCHAINで指定したバージョン
require (
~~
)
このケースではgo.modは更新されているが、autoやpathが付いている場合との違いとして、使用しているツールチェーンが自動で切り替わってはいないということ(成功時も指定したツールチェーン1.23.0が使われている)。
念を押していうが、使用するツールチェーンの自動切り替えはauto, pathを指定しないと起こらない。
go.modのディレクティブの更新とツールチェーンの自動切り替えは別で発生することがあるということを覚えておきたい。
go.workの自動更新
go.modと同様にgo.workにもツールチェーンの設定をすることができる。
ディレクティブ | 意味 |
---|---|
go | go.workファイルが動作するためのgoツールチェーンのバージョン |
toolchain | ワークスペースで使用する推奨ツールチェーンバージョン。<name>のツールチェーンが推奨ツールチェーンより古い場合にのみ効果がある |
例えば、go work useでワークスペースにモジュールを追加する際に、対象のモジュールのgoバージョンの要求がgo.workのgo <version>より高いとする。
この時、auto, pathをつけて実行すると下記のように動作してgo.workの更新される。
# go1.22.2でワークスペースを作成
$ GOTOOLCHAIN=go1.22.2 go work init
$ cat go.work
go 1.22.2
# go1.23.0を要求するgo-mod-sampleをワークスペースに追加
$ cat ./go-mod-sample/go.mod
module go-mod-samplego 1.23.0
toolchain go1.23.3
# ワークスペースのgo <version>がモジュールより低いので失敗
$ GOTOOLCHAIN=go1.22.2 go work use go-mod-sample
go: go-mod-sample/go.mod requires go >= 1.23.0 (running go 1.22.2; GOTOOLCHAIN=go1.22.2)
# autoをつけたので自動でダウンロードしてgo work useを実行成功
$ GOTOOLCHAIN=go1.22.2+auto go work use go-mod-sample
go: worker/go.mod requires go >= 1.23.0; switching to go1.23.6
# go.workが自動更新される
# go <version>はワークスペースモジュールの最大go <version>に
# toolchain <tname>はgo.mod同様に最小バージョン選択の原則で選択される
$ cat go.work
go 1.23.0
toolchain go1.23.6
use ./go-mod-sample
他にもauto, pathを付与されている場合にgo.workが更新されるシーンがある。
あるモジュールAがgo getでインポートしたモジュールBがモジュールAのgo <version>よりも高いとgo.modを更新すると言ったが、go.workのgo <version>がモジュールBより低い場合は同時に更新される(ただし、ローカルで試したらgo mod tidyの時はgo.workが更新されなかった)。
最後に、go.workとgo.modのどちらのtoolchain設定が優先されるか試してみたところ、go.workが優先されるようだった。
# go.work toolchain => go.1.23.0
$ cat go.work
go 1.22.10
toolchain go1.23.0
use ./go-mod-sample
# go.mod toolchain => go1.23.6
$ cat ./go-mod-sample/go.mod
module go-mod-sample
go 1.22.10
toolchain go1.23.6
# go.workのtoolchainが使用される
$ GOTOOLCHAIN=go1.21.1+auto go version
go version go1.23.0 darwin/arm64
念のためコマンドの実行ディレクトリをgo-mod-sampleディレクトリにしたり、go.workとgo.modのtoolchainバージョンの値を入れ替えてみたりしたが結果は変わらず。
go.workとgo.modを組み合わせた時の挙動は情報が少ないのでもっと知りたくなったら動かしてみることにする。
最後に
Toolchainの仕組みはパターンが多いので公式見ながらローカルで動かして確認したがなかなか苦労した。
よければこのブログの内容を参考にしてください。
ではまた。
Discussion