Goブログ

Goモジュールへの移行

Jean de Klerk
2019年8月21日

はじめに

この記事はシリーズのパート2です。

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

Goプロジェクトでは、さまざまな依存関係管理戦略が使用されています。ベンダーリングツール(depglideなど)は人気がありますが、動作に大きな違いがあり、必ずしも連携がうまくいきません。一部のプロジェクトでは、GOPATHディレクトリ全体を単一のGitリポジトリに保存しています。他のプロジェクトでは、単純にgo getに依存し、GOPATHに比較的新しいバージョンの依存関係がインストールされることを期待しています。

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

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

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

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

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

最初のケースは、Goモジュールの使用で説明されています。この記事では、後者の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 -mgo 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 initmodulegoディレクティブのみを含む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ソースファイルに埋め込まれたデータに変換する必要がある場合があります。

テストがGOPATHモードで実行されるテスト内のgoコマンドを期待している場合、失敗する可能性があります。この場合は、テスト対象のソースツリーに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/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"に変更する必要がある場合があります。

モジュールの正規パスがリポジトリパスと異なる別のシナリオとして、Goモジュールのメジャーバージョンが2以上の場合が挙げられます。メジャーバージョンが1より大きいGoモジュールは、モジュールパスにメジャーバージョンのサフィックスを含める必要があります。例えば、バージョンv2.0.0はサフィックス/v2を持つ必要があります。しかし、import文では、モジュール内のパッケージをサフィックスなしで参照している場合があります。例えば、v2.0.1github.com/russross/blackfriday/v2をモジュールを使用しないユーザーがgithub.com/russross/blackfridayとしてインポートしていた場合、インポートパスを/v2サフィックスを含むように更新する必要があります。

結論

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

Goにおける依存関係管理の将来を形作るためのフィードバックやご協力をお願いします。バグレポートまたは体験レポートをお送りください。

モジュールの改善にご協力とフィードバックをいただきありがとうございます。

次の記事: モジュールミラーとチェックサムデータベースの公開
前の記事: Contributors Summit 2019
ブログインデックス