非公開モジュールやローカルで変更したモジュールをGo Modules(vgo)環境で利用する3つの方法

こんにちは、アプリ部の門多です。

GoモジュールはGo 1.11から利用可能な公式の依存性管理ツールです。これはgo.modファイルにモジュールとバージョンを記録しておき、ビルド結果を固定するために使いますが、通常は、参照するモジュールのバージョンが公開されている必要があります。例えば、

module github.com/lufia/mod

require (
    github.com/lufia/backoff v1.1.0
    github.com/lufia/httpclientutil v1.0.0
)

のような依存関係を持つ場合、2つのモジュールは適切な形で参照できる状態でなければいけません。手元の $GOPATH/src に存在していたとしても参照されないので、リポジトリで公開され、適切なタグが必要です。

一般的にはそれほど困りませんが、オリジナルのパッケージにバグがあって修正したけれどマージされていない という場合には、モジュールを公開するまでビルドが行えなくなってしまって、問題となります。または、モジュールのアップデートは必要だけど製品のリリースまでは公開したくない 場合にも同じ問題が発生します。

今回困ったのは後者で、具体的には、いま担当しているプロダクトではgRPCのプロトコル定義を公開していますが、これに新しいフィールドを追加することになりました。ですが、製品リリース前にアップデートすると「フィールドがあったので使ったけど使われていない」みたいな問題が発生するため、なるべくリリースよりも先に公開したくありません。しかしそうすると、開発中に、開発者やCIが取得するモジュールはまだフィールドが追加される前のものなので、新しいフィールドを使ったコードのビルドがエラーになってしまいます。

結局は go.mod ファイルで replace ディレクティブを使う方法が良いと思いますが、いくつか方法があったので調べた内容をまとめました。

vendorディレクトリを使う

Go 1.11以前のコンパイラを使ったビルドやオフライン環境でのビルドをサポートするため、リポジトリルートの vendor ディレクトリはGoモジュール環境でも使えます。go mod vendor を実行すると、依存パッケージを集めて vendor ディレクトリへコピーします。また、go build -mod=vendorvendor を使ったビルドを行います。

$ go mod vendor
$ go test -mod=vendor all
$ go build -mod=vendor

この方法は、

  • 公開するパッケージと vendor ディレクトリの同期が面倒
  • 全ての依存パッケージを vendor に持っておく必要がある
  • リポジトリが肥大化する

などの課題がありますが、

  • ビルド時の通信を抑えることができる
  • Go 1.11以前の環境でも同じ成果物が保証できる

など状況によっては便利です。

GOPROXY経由で参照する

これは本来、モジュールをキャッシュするために用意された仕組みのようです。

GOPROXY 環境変数を設定しておくと、そこからモジュールをダウンロードさせることができます。環境変数で参照した先は決まった構造を持っている必要があります。例えば github.com/lufia/backoff モジュールの場合はこのようになります。

$GOPROXY/
    github.com/lufia/
        backoff/
            @v/
                list
                v1.1.0.info
                v1.1.0.mod
                v1.1.0.zip
                v1.2.0.info
                v1.2.0.mod
                v1.2.0.zip

list はバージョンを1行に1つ書いたテキストファイルです。

v1.1.0
v1.2.0

.mod は該当モジュールの go.mod のようです。

module github.com/lufia/backoff

.info はバージョンやコミットハッシュなどを含んだJSONファイルです。

{"Version":"v1.1.0","Name":"fd4178e0ad4e506e857272ff890a213aadc29c30","Short":"fd4178e0ad4e","Time":"2018-06-25T07:39:12Z"}

.zip はモジュールのソースコードをzipしたものです。

モジュールを事前にダウンロードしたことがあれば、$GOPATH/pkg/mod/cache/download 以下がこの構造になっています。$GOPROXYfile:// も使えるので、手元にキャッシュがあればexport GOPROXY=file:///$GOPATH/pkg/mod/cache/downloadとすればそのまま使えます。この方法は、

  • キャッシュの管理がとてもめんどくさい
  • 開発中のバージョンもなんらかの方法でキャッシュに入れておく必要がある

などの課題もありますが、以下のメリットもあります。

  • vendor と同じように完全なオフライン環境でもビルドが可能
  • リポジトリの肥大化が発生しない

replaceディレクティブを使う

go.modreplace ディレクティブを使うと、リポジトリの参照先を異なるリポジトリまたはローカルファイルへ切り替えることができます。replace の書き方はこのようになります。

module github.com/lufia/mod

require (
    github.com/lufia/backoff v1.1.0
    github.com/lufia/httpclientutil v1.0.0
)

replace (
    github.com/lufia/backoff v1.1.0 => github.com/lufia/backoff-devel v1.2.0

    // ローカルファイルを参照する場合
    //github.com/lufia/backoff v1.1.0 => ./backoff-devel
)

replaceを使った場合は、

  • リポジトリやブランチを複数管理する必要がある
  • リリース後も replace ディレクティブが意図せず残り続ける可能性がある(消し忘れなど)

など、運用で気をつけなければならない点はあります。

まとめ

未公開のモジュールまたはバージョンを使ってビルドするための方法をいくつか調べました。それぞれが解決しようとしている問題はそもそも別のもので、何を解決したいのかを正しく判断しないといけないなと感じました。例えばGo 1.10以前でもビルド結果を保証したい場合は vendor ディレクトリを使うでしょうし、ローカルだけでビルドしたい場合には $GOPROXY が使えるかもしれません。replace ディレクティブについては、一時的な置き換えならいいと思いますが、オリジナルの作者が不在で更新されなくなってしまった場合はimportするモジュールを変更した方が良い場合もあるでしょう。

Go 1.11の時点で、モジュールは導入されたばかりでExperimentalなので、なるべく積極的に共有していきたいなと思います。とりあえず公式のWikiが情報量多くて素晴らしい。

参考リンク