Goモジュールへの移行
Migrating to Go Modules by Jean Barkhuysen
はじめに
本記事はシリーズの第2回です。
- 第1回:Go Modulesの使い方
- 第2回:Goモジュールへの移行(本記事)
- 第3回:Goモジュールの公開
- 第4回:Goモジュール:v2とその先へ
- 第5回:モジュールの互換性を保つ
注記: ドキュメントについては依存関係の管理と モジュールの開発と公開を参照してください。
Goのプロジェクトでは、依存関係管理の手法として実に様々なものが使われています。
depやglideといった
ベンダリングツールがよく使われていますが、
それぞれ振る舞いに大きな違いがあり、常にうまく連携するとは限りません。プロジェクトによっては
GOPATHディレクトリ全体を1つのGitリポジトリに格納しているものもあります。また、単にgo getに頼り、
GOPATHにかなり新しいバージョンの依存関係がインストールされていることを前提にしているプロジェクトもあります。
Go 1.11で導入されたGoのモジュールシステムは、goコマンドに組み込まれた公式の依存関係管理の仕組みを
提供します。この記事では、プロジェクトをモジュールに変換するためのツールとテクニックを解説します。
ご注意ください。もしあなたのプロジェクトがすでにv2.0.0以上のタグを付けられているなら、go.modファイルを
追加する際にモジュールパスを更新する必要があります。v2以降に焦点を当てた今後の記事で、ユーザーに影響を
与えずにその方法を説明する予定です。
プロジェクトでGoモジュールへ移行する
Goモジュールへの移行を始める際、プロジェクトは次の3つの状態のいずれかにあると考えられます。
- まったく新しいGoプロジェクト
- モジュール以外の依存関係管理ツールを使っている、既存のGoプロジェクト
- 依存関係管理ツールをまったく使っていない、既存のGoプロジェクト
1つめのケースについてはGo Modulesの使い方で扱っているので、 本記事では残りの2つについて説明します。
依存関係管理ツールを使っている場合
すでに依存関係管理ツールを使っているプロジェクトを変換するには、次のコマンドを実行します。
$ git clone https://github.com/my/project
[...]
$ cd project
$ cat Godeps/Godeps.json
{
"ImportPath": "github.com/my/project",
"GoVersion": "go1.12",
"GodepVersion": "v80",
"Deps": [
{
"ImportPath": "rsc.io/binaryregexp",
"Comment": "v0.2.0-1-g545cabd",
"Rev": "545cabda89ca36b48b8e681a30d9d769a30b3074"
},
{
"ImportPath": "rsc.io/binaryregexp/syntax",
"Comment": "v0.2.0-1-g545cabd",
"Rev": "545cabda89ca36b48b8e681a30d9d769a30b3074"
}
]
}
$ go mod init github.com/my/project
go: creating new go.mod: module github.com/my/project
go: copying requirements from Godeps/Godeps.json
$ cat go.mod
module github.com/my/project
go 1.12
require rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
$
go mod initは新しいgo.modファイルを作成し、Godeps.jsonやGopkg.lock、あるいは他にもいくつかの
サポートされている形式
から自動的に依存関係をインポートします。go mod initの引数はモジュールパス、つまりそのモジュールが
見つかる場所です。
ここで一旦立ち止まってgo build ./...とgo test ./...を実行するとよいでしょう。この後の手順で
go.modファイルが変更されることがあるため、反復的なアプローチを好むのであれば、この時点のgo.mod
ファイルがモジュール移行前の依存関係の指定にもっとも近い状態になります。
$ go mod tidy
go: downloading rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
go: extracting rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
$ cat go.sum
rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca h1:FKXXXJ6G2bFoVe7hX3kEX6Izxw5ZKRH57DFBJmHCbkU=
rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
$
go mod tidyはモジュール内のパッケージから推移的にインポートされているすべてのパッケージを
見つけ出します。既知のどのモジュールからも提供されていないパッケージについては新たにモジュールの
要求を追加し、逆にインポートされているパッケージを提供していないモジュールについては要求を削除します。
あるモジュールが提供するパッケージが、まだモジュールに移行していないプロジェクトからしかインポート
されていない場合、そのモジュールの要求には// indirectというコメントが付けられます。go.modファイルを
バージョン管理にコミットする前には、常にgo mod tidyを実行しておくのがよい習慣です。
最後に、コードがビルドでき、テストが通ることを確認しましょう。
$ go build ./...
$ go test ./...
[...]
$
他の依存関係管理ツールは、(モジュールではなく)個々のパッケージやリポジトリ全体の単位で依存関係を
指定することがあり、また依存先のgo.modファイルで指定された要求を一般には認識しません。そのため、
以前とまったく同じバージョンのパッケージが得られるとは限らず、破壊的変更を含むバージョンまで
アップグレードしてしまうリスクもあります。したがって、上記のコマンドを実行したあとは、結果として
得られた依存関係を監査することが重要です。そのためには、次のコマンドを実行します。
$ go list -m all
go: finding rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
github.com/my/project
rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca
$
そして、得られたバージョンを以前の依存関係管理ファイルと比較し、選択されたバージョンが適切かどうかを
確認してください。もし望んでいないバージョンが見つかった場合は、go mod why -mやgo mod graphを
使ってその理由を調べ、go getを使って正しいバージョンにアップグレードまたはダウングレードできます。
(要求したバージョンが以前選択されていたバージョンよりも古い場合、go getは互換性を保つために
必要に応じて他の依存関係もダウングレードします。)たとえば次のようになります。
$ go mod why -m rsc.io/binaryregexp
[...]
$ go mod graph | grep rsc.io/binaryregexp
[...]
$ go get rsc.io/binaryregexp@v0.2.0
$
依存関係管理ツールを使っていない場合
依存関係管理システムを使っていないGoプロジェクトでは、まずgo.modファイルを作成するところから
始めます。
$ git clone https://go.googlesource.com/blog
[...]
$ cd blog
$ go mod init golang.org/x/blog
go: creating new go.mod: module golang.org/x/blog
$ cat go.mod
module golang.org/x/blog
go 1.12
$
以前の依存関係管理ツールの設定ファイルがない場合、go mod initはmoduleディレクティブとgo
ディレクティブのみを持つgo.modファイルを作成します。この例では、モジュールパスをgolang.org/x/blog
に設定していますが、これはこのプロジェクトの
カスタムインポートパスがそれだからです。
ユーザーはこのパスでパッケージをインポートする可能性があるため、パスを変更しないよう注意しなければ
なりません。
moduleディレクティブはモジュールパスを宣言し、goディレクティブはそのモジュール内のコードの
コンパイルに使うことが期待されるGo言語のバージョンを宣言します。
次に、go mod tidyを実行してモジュールの依存関係を追加します。
$ go mod tidy
go: finding golang.org/x/website latest
go: finding gopkg.in/tomb.v2 latest
go: finding golang.org/x/net latest
go: finding golang.org/x/tools latest
go: downloading github.com/gorilla/context v1.1.1
go: downloading golang.org/x/tools v0.0.0-20190813214729-9dba7caff850
go: downloading golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7
go: extracting github.com/gorilla/context v1.1.1
go: extracting golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7
go: downloading gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
go: extracting gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
go: extracting golang.org/x/tools v0.0.0-20190813214729-9dba7caff850
go: downloading golang.org/x/website v0.0.0-20190809153340-86a7442ada7c
go: extracting golang.org/x/website v0.0.0-20190809153340-86a7442ada7c
$ cat go.mod
module golang.org/x/blog
go 1.12
require (
github.com/gorilla/context v1.1.1
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7
golang.org/x/text v0.3.2
golang.org/x/tools v0.0.0-20190813214729-9dba7caff850
golang.org/x/website v0.0.0-20190809153340-86a7442ada7c
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
)
$ cat go.sum
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
git.apache.org/thrift.git v0.0.0-20181218151757-9b75e4fe745a/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
[...]
$
go mod tidyは、モジュール内のパッケージから推移的にインポートされているすべてのパッケージについて
モジュールの要求を追加し、特定バージョンの各ライブラリのチェックサムを含むgo.sumを構築しました。
最後に、コードが引き続きビルドでき、テストが通ることを確認しましょう。
$ go build ./...
$ go test ./...
ok golang.org/x/blog 0.335s
? golang.org/x/blog/content/appengine [no test files]
ok golang.org/x/blog/content/cover 0.040s
? golang.org/x/blog/content/h2push/server [no test files]
? golang.org/x/blog/content/survey2016 [no test files]
? golang.org/x/blog/content/survey2017 [no test files]
? golang.org/x/blog/support/racy [no test files]
$
go mod tidyが要求を追加する際には、そのモジュールの最新バージョンを追加することに注意してください。
もしGOPATHに含まれていた依存関係の古いバージョンが、その後破壊的変更を公開していた場合、
go mod tidyやgo build、go testでエラーが発生することがあります。その場合は、go getを
使って古いバージョンにダウングレードするか(たとえばgo get github.com/broken/module@v1.1.0の
ように)、時間をかけて各依存関係の最新バージョンに対応させてください。
モジュールモードでのテスト
Goモジュールへ移行した後、一部のテストは調整が必要になることがあります。
テストがパッケージディレクトリにファイルを書き込む必要がある場合、パッケージディレクトリが
読み取り専用のモジュールキャッシュ内にあるとテストが失敗することがあります。特に、これが原因で
go test allが失敗することがあります。この場合、テストは書き込みが必要なファイルを一時ディレクトリに
コピーしてから使うべきです。
テストが他のパッケージ内のファイルを見つけて読み込むために相対パス(../package-in-another-module)
に依存している場合、そのパッケージが別のモジュールにあると失敗します。別のモジュールは、モジュール
キャッシュ内のバージョン付きサブディレクトリか、replaceディレクティブで指定されたパスに配置される
ためです。このような場合、テスト用の入力ファイルを自分のモジュール内にコピーするか、生のファイル
ではなく.goソースファイルに埋め込んだデータに変換する必要があるかもしれません。
テストの中で実行されるgoコマンドがGOPATHモードで動作することを期待している場合、失敗することが
あります。このような場合は、テスト対象のソースツリーにgo.modファイルを追加するか、明示的に
GO111MODULE=offを設定する必要があるかもしれません。
リリースを公開する
最後に、新しいモジュールのリリースバージョンにタグを付けて公開しましょう。まだ一度もバージョンを リリースしていないのであればこれは任意ですが、公式なリリースがないと、下流のユーザーは 疑似バージョンを使って特定のコミットに依存することに なり、サポートがより難しくなる可能性があります。
$ git tag v1.2.0
$ git push origin v1.2.0
新しいgo.modファイルは、モジュールの正規のインポートパスを定義し、新しい最小バージョン要求を
追加します。ユーザーがすでに正しいインポートパスを使っていて、依存関係に破壊的変更がなければ、
go.modファイルの追加は後方互換になります。とはいえこれは重要な変更であり、既存の問題を表面化
させる可能性があります。既存のバージョンタグがある場合は、
マイナーバージョンを上げるべきです。バージョンを上げて公開する
方法についてはGoモジュールの公開を参照してください。
インポートと正規のモジュールパス
各モジュールは自身のgo.modファイルの中でモジュールパスを宣言します。モジュール内のパッケージを
参照する各import文は、パッケージパスの接頭辞としてそのモジュールパスを持たなければなりません。
しかしgoコマンドは、モジュールを含むリポジトリに、さまざまな異なる
リモートインポートパスを通じて出会うことが
あります。たとえば、golang.org/x/lintとgithub.com/golang/lintはどちらも
go.googlesource.com/lintでホストされているコードを含む
リポジトリに解決されます。そのリポジトリに含まれる
go.modファイルは自身のパスを
golang.org/x/lintと宣言しているため、そのパスだけが有効なモジュールに対応します。
Go 1.4では、// importコメントを使って正規の
インポートパスを宣言する仕組みが提供されましたが、パッケージの作者が必ずしもそれを用意していた
わけではありません。その結果、モジュール以前に書かれたコードでは、不一致によるエラーが表面化しない
まま、モジュールの非正規なインポートパスが使われていることがあります。モジュールを使う場合、
インポートパスは正規のモジュールパスと一致していなければならないため、import文を更新する必要が
あるかもしれません。たとえば、import "github.com/golang/lint"をimport "golang.org/x/lint"に
変更する必要があるかもしれません。
モジュールの正規のパスがリポジトリのパスと異なるもう1つのケースは、メジャーバージョンが2以上の
Goモジュールで発生します。メジャーバージョンが1より大きいGoモジュールは、モジュールパスにメジャー
バージョンの接尾辞を含めなければなりません。たとえば、バージョンv2.0.0には/v2という接尾辞が
必要です。しかし、import文はその接尾辞 なし でモジュール内のパッケージを参照していることが
あります。たとえば、v2.0.1のgithub.com/russross/blackfriday/v2をモジュールを使わずに利用して
いたユーザーは、代わりにgithub.com/russross/blackfridayとしてインポートしていた可能性があり、
その場合は/v2接尾辞を含むようにインポートパスを更新する必要があります。
まとめ
Goモジュールへの変換は、多くのユーザーにとって単純明快なプロセスであるはずです。非正規の インポートパスや依存関係内の破壊的変更が原因で、時折問題が発生することもあります。今後の記事では、 新しいバージョンの公開、v2以降、そして奇妙な状況を デバッグする方法について解説していきます。
フィードバックを提供し、Goにおける依存関係管理の将来を形作る手助けをしていただくために、 バグ報告や経験談の報告を お送りください。
モジュールの改善にあたって、これまでいただいたすべてのフィードバックとご協力に感謝します。
By Jean Barkhuysen