Goモジュール管理下では、プロジェクトで使うGo製ツールのバージョンも管理できます。今までの経験では、ツールのバージョンが上がって困ることは記憶にないですが、とはいえ2018年5月ごろにprotoc-gen-goが大きめの変更を入れたこともあるので、バージョン管理しておいて損はないでしょう。このハックは、割とGoモジュール初期からあったようですが、最近使ったので書きました。
- Go 1.11 Modules - How can I track tool dependencies for a module?
- Go modules by example - Tools as dependencies
使い方
ツールを追加する
Go 1.13時点では、モジュール管理しているリポジトリでgoimportsなどのツールをgo get
すると、go.modが書き換えられて管理対象に入ります*1が、恒久的にソースコードへ含まれる訳ではないため、go mod tidy
などで整理すると、ツールのインストール時に追加されたモジュールがgo.modから削除されます。ここではツールもバージョン管理したいので、ビルド制約(build constraints)でビルド対象に含まれないようにしたファイルに、利用するツールを書き並べていきます。このときのビルドタグやファイル名はなんでも構いませんが、公式ではファイル名にtools.go、ビルドタグにtoolsが使われているので、合わせておいくといいでしょう。
// +build tools package main import ( // コマンドまでのパスを書く _ "golang.org/x/tools/cmd/stringer" _ "golang.org/x/lint/golint" )
これでgo mod tidy
すると、その時点の最新バージョンがGoモジュール管理対象に入ります。または最初からバージョンを指定したい場合はgo get
で明示しましょう。
// 全部最新でいい場合 % go mod tidy // バージョン指定する場合 % go get golang.org/x/lint/golint@v0.0.0... % git add go.mod go.sum
なぜこれで動くのかというと、tools.goはビルド制約があるのでビルド対象には含まれません。しかしgo mod tidy
はビルド制約に関わらずモジュールを探すので、安全にツールのバージョンをgo.modに残せます。
ツールをインストールする
CIなど、go.modで指定されたバージョンをインストールしたい場合は、go install
を使います。
% go install golang.org/x/lint/golint
バージョンを維持したい場合は必ずgo install
を使いましょう。go get
は現在参照可能な最新のバージョンをインストールするため、go.modが更新されてしまいます。
ツールをアップデートする
この場合はgo get
で更新すればいいですね。
// バージョン確認 % go list -m -u all // 以下のうちどれかでアップデート % go get golang.org/x/lint/golint % go get -u golang.org/x/lint/golint % go get -u=patch golang.org/x/lint/golint % git add go.mod go.sum
go getの使い分け
Goモジュールではgo get
だけでgo.modに関わらず最新のバージョンを取得するので、go get -u
との違いについてGOPATHモードのgo get
を知っている人は混乱するかもしれません。これはgo help module-getによると、インストールするモジュールが依存するモジュールをどう扱うか、を表すようです。
go get <pkg>
: <pkg>のgo.modに書かれたバージョンをminimal version selectionで維持するgo get -u <pkg>
: <pkg>が依存するモジュールも同じメジャーバージョン内でアップデートするgo get -u=patch <pkg>
: <pkg>が依存するモジュールも同じマイナーバージョン内でアップデートする
アップデートしたいなんらかの事情があるとか、モジュール自体の更新が滞ってない限りはgo get
を使えば良さそうですね。また、go get
とgo install
の違いはバージョンを更新するかどうか、です。
go get <pkg>
: 最新の<pkg>でgo.modを更新するgo install <pkg>
: メインリポジトリのgo.modに従う
つらいところ
Goモジュールを通してつらい部分は、ツールのバージョン管理でもそのまま残ります。具体的には、ツールがセマンティックバージョニングを守っていない場合、公式のモジュールデータベースにバージョンが記録されないのでGOPRIVATEやGONOSUMDB環境変数を使ってリポジトリを直接見に行く必要があります。
例えばgithub.com/github/hubは2020年2月時点で、モジュールデータベース上はv2.11.2+incompatibleが最新バージョンとなっていますが、GitHubのタグではv2.14.1まで存在します。そのため、v2.14.1をインストールしたい場合、以下のようにバージョンを直接書き換えたうえで公式データベースを参照しないよう回避しなければなりません。
% go mod edit -require github.com/github/hub@v2.14.1+incompatible % GONOSUMDB=github.com/github/hub go install github.com/github/hub // go getするとダメ
ところで、hubの新しいバージョンが公式モジュールデータベースに記録されない理由ですが、このモジュールはv2.12.0でgo.modを持つように変わりました。そうすると、タグ付けされたバージョンがv2以上であるのにimport pathがgithub/hub/v2のような形になっていないためincompatibleなバージョンとして扱われます。現在、公式モジュールデータベースはgo.mod対応モジュールではincompatibleなバージョンを扱わないようで、その結果としてv2.12.0以降のバージョンが登録されなくなってしまっているようです。
これは手元で以下のコマンドを実行してみると分かります。
% go version go version go1.13.5 darwin/amd64 % go get github.com/github/hub@v2.14.1 go: finding github.com/github/hub v2.14.1 go: finding github.com/github/hub v2.14.1 go get github.com/github/hub@v2.14.1: github.com/github/hub@v2.14.1: invalid version: module contains a go.mod file, so major version must be compatible: should be v0 or v1, not v2
どう使うと良いのか
冒頭で書いたように、Go製ツールのバージョンが上がって致命的に困ったことは今のところありません。なのでツールのバージョンにそれほど神経質になる必要はない気がします。また、手元を古いバージョンで固定するのはあまり筋が良くないので、開発者の手元では新しいバージョンを使うようにして、CI側のバージョンを固定しておくとツールの最新バージョンでCIが誰にも気づかれず突然壊れることがなくなって便利かな、と思います。
*1:golang/go#30515でgo.modを更新しないオプションも検討されています