> Source: https://www.ymotongpoo.com/works/goblog-ja/migrating-to-go-modules/


# Goモジュールへの移行

[Migrating to Go Modules](https://go.dev/blog/migrating-to-go-modules) by Jean Barkhuysen

## はじめに

本記事はシリーズの第2回です。

* 第1回：[Go Modulesの使い方](https://go.dev/blog/using-go-modules)
* **第2回：Goモジュールへの移行**（本記事）
* 第3回：[Goモジュールの公開](https://go.dev/blog/publishing-go-modules)
* 第4回：[Goモジュール：v2とその先へ](https://go.dev/blog/v2-go-modules)
* 第5回：[モジュールの互換性を保つ](https://go.dev/blog/module-compatibility)

**注記：** ドキュメントについては[依存関係の管理](https://go.dev/doc/modules/managing-dependencies)と
[モジュールの開発と公開](https://go.dev/doc/modules/developing)を参照してください。

Goのプロジェクトでは、依存関係管理の手法として実に様々なものが使われています。
[dep](https://github.com/golang/dep)や[glide](https://github.com/Masterminds/glide)といった
[ベンダリング](https://go.dev/cmd/go/#hdr-Vendor_Directories)ツールがよく使われていますが、
それぞれ振る舞いに大きな違いがあり、常にうまく連携するとは限りません。プロジェクトによっては
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の使い方](https://go.dev/blog/using-go-modules)で扱っているので、
本記事では残りの2つについて説明します。

## 依存関係管理ツールを使っている場合

すでに依存関係管理ツールを使っているプロジェクトを変換するには、次のコマンドを実行します。

```shell-session
$ 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`、あるいは他にもいくつかの
[サポートされている形式](https://go.googlesource.com/go/+/362625209b6cd2bc059b6b0a67712ddebab312d9/src/cmd/go/internal/modconv/modconv.go#9)
から自動的に依存関係をインポートします。`go mod init`の引数はモジュールパス、つまりそのモジュールが
見つかる場所です。

ここで一旦立ち止まって`go build ./...`と`go test ./...`を実行するとよいでしょう。この後の手順で
`go.mod`ファイルが変更されることがあるため、反復的なアプローチを好むのであれば、この時点の`go.mod`
ファイルがモジュール移行前の依存関係の指定にもっとも近い状態になります。

```shell-session
$ 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`を実行しておくのがよい習慣です。

最後に、コードがビルドでき、テストが通ることを確認しましょう。

```shell-session
$ go build ./...
$ go test ./...
[...]
$
```

他の依存関係管理ツールは、（モジュールではなく）個々のパッケージやリポジトリ全体の単位で依存関係を
指定することがあり、また依存先の`go.mod`ファイルで指定された要求を一般には認識しません。そのため、
以前とまったく同じバージョンのパッケージが得られるとは限らず、破壊的変更を含むバージョンまで
アップグレードしてしまうリスクもあります。したがって、上記のコマンドを実行したあとは、結果として
得られた依存関係を監査することが重要です。そのためには、次のコマンドを実行します。

```shell-session
$ 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`は互換性を保つために
必要に応じて他の依存関係もダウングレードします。）たとえば次のようになります。

```shell-session
$ 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`ファイルを作成するところから
始めます。

```shell-session
$ 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`
に設定していますが、これはこのプロジェクトの
[カスタムインポートパス](https://go.dev/cmd/go/#hdr-Remote_import_paths)がそれだからです。
ユーザーはこのパスでパッケージをインポートする可能性があるため、パスを変更しないよう注意しなければ
なりません。

`module`ディレクティブはモジュールパスを宣言し、`go`ディレクティブはそのモジュール内のコードの
コンパイルに使うことが期待されるGo言語のバージョンを宣言します。

次に、`go mod tidy`を実行してモジュールの依存関係を追加します。

```shell-session
$ 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`を構築しました。
最後に、コードが引き続きビルドでき、テストが通ることを確認しましょう。

```shell-session
$ 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`を設定する必要があるかもしれません。

## リリースを公開する

最後に、新しいモジュールのリリースバージョンにタグを付けて公開しましょう。まだ一度もバージョンを
リリースしていないのであればこれは任意ですが、公式なリリースがないと、下流のユーザーは
[疑似バージョン](https://go.dev/cmd/go/#hdr-Pseudo_versions)を使って特定のコミットに依存することに
なり、サポートがより難しくなる可能性があります。

```shell-session
$ git tag v1.2.0
$ git push origin v1.2.0
```

新しい`go.mod`ファイルは、モジュールの正規のインポートパスを定義し、新しい最小バージョン要求を
追加します。ユーザーがすでに正しいインポートパスを使っていて、依存関係に破壊的変更がなければ、
`go.mod`ファイルの追加は後方互換になります。とはいえこれは重要な変更であり、既存の問題を表面化
させる可能性があります。既存のバージョンタグがある場合は、
[マイナーバージョン](https://semver.org/#spec-item-7)を上げるべきです。バージョンを上げて公開する
方法については[Goモジュールの公開](https://go.dev/blog/publishing-go-modules)を参照してください。

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

各モジュールは自身の`go.mod`ファイルの中でモジュールパスを宣言します。モジュール内のパッケージを
参照する各`import`文は、パッケージパスの接頭辞としてそのモジュールパスを持たなければなりません。
しかし`go`コマンドは、モジュールを含むリポジトリに、さまざまな異なる
[リモートインポートパス](https://go.dev/cmd/go/#hdr-Remote_import_paths)を通じて出会うことが
あります。たとえば、`golang.org/x/lint`と`github.com/golang/lint`はどちらも
[go.googlesource.com/lint](https://go.googlesource.com/lint)でホストされているコードを含む
リポジトリに解決されます。そのリポジトリに含まれる
[`go.mod`ファイル](https://go.googlesource.com/lint/+/refs/heads/master/go.mod)は自身のパスを
`golang.org/x/lint`と宣言しているため、そのパスだけが有効なモジュールに対応します。

Go 1.4では、[`// import`コメント](https://go.dev/cmd/go/#hdr-Import_path_checking)を使って正規の
インポートパスを宣言する仕組みが提供されましたが、パッケージの作者が必ずしもそれを用意していた
わけではありません。その結果、モジュール以前に書かれたコードでは、不一致によるエラーが表面化しない
まま、モジュールの非正規なインポートパスが使われていることがあります。モジュールを使う場合、
インポートパスは正規のモジュールパスと一致していなければならないため、`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モジュールへの変換は、多くのユーザーにとって単純明快なプロセスであるはずです。非正規の
インポートパスや依存関係内の破壊的変更が原因で、時折問題が発生することもあります。今後の記事では、
[新しいバージョンの公開](https://go.dev/blog/publishing-go-modules)、v2以降、そして奇妙な状況を
デバッグする方法について解説していきます。

フィードバックを提供し、Goにおける依存関係管理の将来を形作る手助けをしていただくために、
[バグ報告](https://go.dev/issue/new)や[経験談の報告](https://go.dev/wiki/ExperienceReports)を
お送りください。

モジュールの改善にあたって、これまでいただいたすべてのフィードバックとご協力に感謝します。

By Jean Barkhuysen

