The Go Blog

Go Modulesへの移行

ジャン・バークハイゼン
2019年8月21日

はじめに

この投稿はシリーズのパート2です。

注:ドキュメントについては、依存関係の管理およびモジュールの開発と公開を参照してください。

Goプロジェクトでは、さまざまな依存関係管理戦略が使用されています。Vendoringツール、例えばdepglideは人気がありますが、その動作には大きな違いがあり、常に連携して機能するわけではありません。一部のプロジェクトでは、GOPATHディレクトリ全体を単一のGitリポジトリに保存しています。他のプロジェクトでは、単純にgo getに依存し、比較的最近のバージョンの依存関係がGOPATHにインストールされることを期待しています。

Go 1.11で導入されたGoのモジュールシステムは、goコマンドに組み込まれた公式の依存関係管理ソリューションを提供します。この記事では、プロジェクトをモジュールに変換するためのツールとテクニックについて説明します。

注意:プロジェクトがすでにv2.0.0以降のタグ付けがされている場合、go.modファイルを追加する際にモジュールパスを更新する必要があります。v2以降に焦点を当てた今後の記事で、ユーザーを壊すことなくその方法を説明します。

プロジェクトでGoモジュールに移行する

Goモジュールへの移行を開始する際、プロジェクトは次の3つの状態のいずれかにあります。

  • 真新しいGoプロジェクト。
  • モジュール以外の依存関係マネージャーを持つ確立されたGoプロジェクト。
  • 依存関係マネージャーを持たない確立されたGoプロジェクト。

最初のケースは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.jsonGopkg.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 initmoduleディレクティブと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 tidygo 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 Modulesの公開を参照してください。

インポートと正規のモジュールパス

各モジュールは、そのgo.modファイル内でモジュールパスを宣言します。モジュール内のパッケージを参照する各importステートメントは、パッケージパスのプレフィックスとしてモジュールパスを持つ必要があります。ただし、goコマンドは、多くの異なるリモートインポートパスを通じてモジュールを含むリポジトリに遭遇する可能性があります。例えば、golang.org/x/lintgithub.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ステートメントは、そのサフィックスなしでモジュール内のパッケージを参照していた可能性があります。例えば、github.com/russross/blackfriday/v2v2.0.1の非モジュールユーザーは、それをgithub.com/russross/blackfridayとしてインポートしていた可能性があり、インポートパスを更新して/v2サフィックスを含める必要があります。

まとめ

Goモジュールへの変換は、ほとんどのユーザーにとって簡単なプロセスであるはずです。非正規のインポートパスや依存関係内の破壊的変更により、たまに問題が発生する可能性があります。今後の投稿では、新しいバージョンの公開、v2以降、および奇妙な状況をデバッグする方法について説明します。

Goにおける依存関係管理の将来についてフィードバックを提供し、その形成にご協力いただくには、バグレポートまたは体験レポートをお送りください。

モジュールの改善に対する皆様のフィードバックとご協力に感謝いたします。

次の記事: モジュールミラーとチェックサムデータベースが公開されました
前の記事: コントリビューターサミット2019
ブログインデックス