go getで意図せぬバージョンが選択される話

ツールをgo getでインストールすると 意図しないバージョンのパッケージが使われるケースを紹介します。

TL;DR

  • go get で複数のツールを同時にインストールすると 意図しないバージョンのパッケージが使われることがある
  • ツールは1つずつ go get したほうが良い

経緯

go get がモジュールおよびパッケージを取得・更新するサブコマンドだということはご存じかと思います。

一方でモジュールモードでは go install の代わりに go get がGo製コマンドのインストールコマンドとして機能します。

さらにコマンドインストールのユースケースにおいてバージョンを指定すると、 依存するモジュール群のバージョンを完全に固定してビルドできるという特徴があります。 以下はそのコマンド例です。

$ GOBIN=`pwd` GO111MODULE=on go get github.com/golang/protobuf/protoc-gen-go@v1.4.2
go: found github.com/golang/protobuf/protoc-gen-go in github.com/golang/protobuf v1.4.2

環境変数 GOBIN でビルド済みバイナリのインストール場所を指定し、 GO111MODULEGOPATH 内でなくても強制的にモジュールモードにしています。

この例では現在のディレクトリに protoc-gen-go v1.4.2 のビルド済みバイナリが配置されます。

protoc-gen-go@v1.4.2 は google.golang.com/protobuf@v1.23.0 に依存しています。 そのことは protoc-gen-go@v1.4.2 の go.mod に宣言されています。 参照

ビルド済みバイナリからこのことを確認してみましょう。

$ strings protoc-gen-go | grep '^dep\s'
dep     google.golang.org/protobuf      v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=

確かに golang.golang.protobuf@v1.23.0 を用いています。

さてここで protoc-gen-go@v1.4.2 と一緒にGo製のポピュラーなswaggerツール github.com/go-swagger/go-swagger@v0.25.0 を同時に go get してみましょう。 以下はそのコマンドです。

$ GOBIN=`pwd` GO111MODULE=on go get \
  github.com/golang/protobuf/protoc-gen-go@v1.4.2 \
  github.com/go-swagger/go-swagger/cmd/swagger@v0.25.0
go: found github.com/go-swagger/go-swagger/cmd/swagger in github.com/go-swagger/go-swagger v0.25.0
go: found github.com/golang/protobuf/protoc-gen-go in github.com/golang/protobuf v1.4.2

そしてビルドされた protoc-gen-go の依存を調べるとなんと protobuf@1.25.0 に強制的に変更されてしまっています。 見てみましょう。

$ strings protoc-gen-go | grep '^dep\s'
dep     google.golang.org/protobuf      v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=

protoc-gen-go@v1.4.2 の go.mod が明示的に指定したバージョンを用いてないこと、 また go get をモジュールモードで明示的なバージョン指定を用いたことを考えると、 直感に反した振る舞いと言えるでしょう。

これは go-swagger@v0.25.0 の go.mod が protobuf@v1.25.0 を要求していることが原因です。 参照

go.mod で管理されたモジュール内で複数のモジュールを go get した時の動作だと考えればこれは理に適っています。 Goは依存モジュールについて複数のバージョンが指定されると、それらのうちもっとも新しいものを選択します。

しかしこの動作はGo製コマンドのインストールという観点では go.mod に指定されたバージョンが使われないという直感に反する事態を引き起こします。

これを回避するためにGo製コマンドは1つずつ go get でインストールするほうが良いでしょう。

なお go install をモジュールモードに対応させようという話もあるので (参照) 近い将来、改善されるかもしれません。 知らんけど。

以上今日ハマった go get でのコマンドインストールについてのわかりにくい落とし穴でした。