Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

Goモジュールでツールもバージョン管理する

Goモジュール管理下では、プロジェクトで使うGo製ツールのバージョンも管理できます。今までの経験では、ツールのバージョンが上がって困ることは記憶にないですが、とはいえ2018年5月ごろにprotoc-gen-goが大きめの変更を入れたこともあるので、バージョン管理しておいて損はないでしょう。このハックは、割とGoモジュール初期からあったようですが、最近使ったので書きました。

使い方

ツールを追加する

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 getgo installの違いはバージョンを更新するかどうか、です。

  • go get <pkg>: 最新の<pkg>go.modを更新する
  • go install <pkg>: メインリポジトリgo.modに従う

つらいところ

Goモジュールを通してつらい部分は、ツールのバージョン管理でもそのまま残ります。具体的には、ツールがセマンティックバージョニングを守っていない場合、公式のモジュールデータベースにバージョンが記録されないのでGOPRIVATEGONOSUMDB環境変数を使ってリポジトリを直接見に行く必要があります。

例えば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.0go.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#30515go.modを更新しないオプションも検討されています