Goモジュールを使う

Using Go Modules by Tyler Bui-Palsulich and Eno Compton

はじめに

この記事はシリーズの第1回です。

注: モジュールを使った依存関係の管理についてのドキュメントは Managing dependencies を参照してください。

Go 1.11およびGo 1.12には、モジュールのプレリミナリサポートが 含まれています。モジュールは、依存関係のバージョン情報を明示的にし、管理しやすくするGoの 新しい依存関係管理システムです。この記事は、モジュールを 使い始めるために必要な基本的な操作を紹介する入門記事です。

モジュールとは、ルートに go.mod ファイルを持つファイルツリーに格納された、 Goパッケージの集まりです。go.mod ファイルは、モジュールの モジュールパス (ルートディレクトリに使われるインポートパスでもあります)と、ビルドを成功させるために 必要な他のモジュールである 依存関係の要求 を定義します。各依存関係の要求は、モジュールパスと 具体的なセマンティックバージョンの組み合わせとして記述されます。

Go 1.11以降、go コマンドは、カレントディレクトリまたはその親ディレクトリのいずれかに go.mod が 存在する場合、そのディレクトリが $GOPATH/src外側 にあれば、モジュールの利用を有効にします。 ($GOPATH/src の内側では、互換性のために、たとえ go.mod が見つかったとしても、go コマンドは 引き続き従来のGOPATHモードで動作します。詳細は goコマンドのドキュメントを参照してください。) Go 1.13以降では、モジュールモードがすべての開発においてデフォルトになる予定です。

この記事では、モジュールを使ってGoのコードを開発する際によく発生する一連の操作を順に見ていきます。

  • 新しいモジュールの作成
  • 依存関係の追加
  • 依存関係のアップグレード
  • 新しいメジャーバージョンへの依存関係の追加
  • 依存関係の新しいメジャーバージョンへのアップグレード
  • 使われていない依存関係の削除

新しいモジュールを作成する

新しいモジュールを作成してみましょう。

$GOPATH/src の外側のどこかに新しい空のディレクトリを作成し、そのディレクトリに cd してから、 新しいソースファイル hello.go を作成します。

package hello

func Hello() string {
    return "Hello, world."
}

テストも書いてみましょう。hello_test.go に書きます。

package hello

import "testing"

func TestHello(t *testing.T) {
    want := "Hello, world."
    if got := Hello(); got != want {
        t.Errorf("Hello() = %q, want %q", got, want)
    }
}

この時点では、ディレクトリにはパッケージは含まれていますが、モジュールはまだ含まれていません。 go.mod ファイルが存在しないからです。もし /home/gopher/hello で作業していて、ここで go test を実行すると、次のようになります。

$ go test
PASS
ok      _/home/gopher/hello 0.020s
$

最後の行はパッケージ全体のテスト結果の要約です。私たちは $GOPATH の外側で作業しており、 かつどのモジュールの外側でもあるため、go コマンドはカレントディレクトリのインポートパスを 知りません。そのため、ディレクトリ名に基づいた偽のインポートパス _/home/gopher/hello を 作り出します。

go mod init を使ってカレントディレクトリをモジュールのルートにし、もう一度 go test を 試してみましょう。

$ go mod init example.com/hello
go: creating new go.mod: module example.com/hello
$ go test
PASS
ok      example.com/hello   0.020s
$

おめでとうございます! あなたは最初のモジュールを書き、テストしました。

go mod init コマンドは go.mod ファイルを書き出しました。

$ cat go.mod
module example.com/hello

go 1.12
$

go.mod ファイルはモジュールのルートにのみ存在します。サブディレクトリ内のパッケージは、 モジュールパスにサブディレクトリへのパスを加えたインポートパスを持ちます。たとえば、 サブディレクトリ world を作成したとしても、そこで go mod init を実行する必要はありませんし、 実行すべきでもありません。そのパッケージは自動的に example.com/hello モジュールの一部として 認識され、インポートパスは example.com/hello/world になります。

依存関係を追加する

Goモジュールを作った主な動機は、他の開発者が書いたコードを利用する(つまり、それに対する 依存関係を追加する)際の体験を改善することでした。

hello.go を更新して rsc.io/quote をインポートし、それを使って Hello を実装してみましょう。

package hello

import "rsc.io/quote"

func Hello() string {
    return quote.Hello()
}

もう一度テストを実行してみましょう。

$ go test
go: finding rsc.io/quote v1.5.2
go: downloading rsc.io/quote v1.5.2
go: extracting rsc.io/quote v1.5.2
go: finding rsc.io/sampler v1.3.0
go: finding golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: downloading rsc.io/sampler v1.3.0
go: extracting rsc.io/sampler v1.3.0
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: extracting golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
PASS
ok      example.com/hello   0.023s
$

go コマンドは、go.mod に記載された特定の依存モジュールのバージョンを使ってインポートを 解決します。go.mod 内のどのモジュールからも提供されていないパッケージのインポートに 遭遇すると、go コマンドは自動的にそのパッケージを含むモジュールを探し出し、最新バージョンを 使って go.mod に追加します。(「最新」とは、タグ付けされた最新の安定版 (プレリリースではない)バージョン、それがなければタグ付け された最新のプレリリースバージョン、それもなければタグ付けされていない最新のバージョンと 定義されます。)今回の例では、go test が新しいインポート rsc.io/quote を解決し、モジュール rsc.io/quote v1.5.2 に対応させました。また、rsc.io/quote が使っている2つの依存関係、 rsc.io/samplergolang.org/x/text もダウンロードしました。go.mod ファイルに記録される のは直接の依存関係のみです。

$ cat go.mod
module example.com/hello

go 1.12

require rsc.io/quote v1.5.2
$

2回目の go test コマンドはこの作業を繰り返しません。go.mod はすでに最新の状態になっており、 ダウンロードされたモジュールはローカル($GOPATH/pkg/mod の中)にキャッシュされているからです。

$ go test
PASS
ok      example.com/hello   0.020s
$

go コマンドによって新しい依存関係の追加は素早く簡単になりますが、それには代償がないわけでは ないことに注意してください。あなたのモジュールは、正確性、セキュリティ、適切なライセンスと いった重要な観点において、文字通りその新しい依存関係に依存することになります。これらはほんの 一例にすぎません。より詳しい考察については、Russ Coxのブログ記事 「Our Software Dependency Problem」を参照してください。

先ほど見たように、1つの直接的な依存関係を追加すると、しばしば他の間接的な依存関係も一緒に 持ち込まれます。コマンド go list -m all は、カレントモジュールとそのすべての依存関係を 一覧表示します。

$ go list -m all
example.com/hello
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
$

go list の出力では、メインモジュール とも呼ばれるカレントモジュールが常に最初の行にあり、 その後にモジュールパス順にソートされた依存関係が続きます。

golang.org/x/text のバージョン v0.0.0-20170915032832-14c0d48ead0c疑似バージョンの一例です。これは、タグ付けされて いない特定のコミットを表すための go コマンドのバージョン構文です。

go.mod に加えて、go コマンドは go.sum というファイルも管理しています。このファイルには、 特定のモジュールバージョンの内容に対する期待される 暗号学的ハッシュ値が 含まれています。

$ cat go.sum
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZO...
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:Nq...
rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3...
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPX...
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/Q...
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9...
$

go コマンドは go.sum ファイルを使って、これらのモジュールを今後ダウンロードする際に、 最初にダウンロードしたものと同じビット列が取得されることを保証します。これにより、悪意による ものであれ、偶発的なものであれ、その他の理由であれ、プロジェクトが依存しているモジュールが 予期せず変化しないことが保証されます。go.modgo.sum の両方をバージョン管理システムに チェックインすべきです。

依存関係をアップグレードする

Goモジュールでは、バージョンはセマンティックバージョンのタグで参照されます。セマンティック バージョンには、メジャー、マイナー、パッチの3つの部分があります。たとえば v0.1.2 では、 メジャーバージョンは0、マイナーバージョンは1、パッチバージョンは2です。ここでは、いくつかの マイナーバージョンのアップグレードを見ていきましょう。次の節では、メジャーバージョンの アップグレードについて考えます。

go list -m all の出力から、golang.org/x/text のタグ付けされていないバージョンを使っている ことがわかります。タグ付けされた最新バージョンにアップグレードして、すべてがまだうまく動くか テストしてみましょう。

$ go get golang.org/x/text
go: finding golang.org/x/text v0.3.0
go: downloading golang.org/x/text v0.3.0
go: extracting golang.org/x/text v0.3.0
$ go test
PASS
ok      example.com/hello   0.013s
$

やった! すべてパスしました。もう一度 go list -m allgo.mod ファイルを見てみましょう。

$ go list -m all
example.com/hello
golang.org/x/text v0.3.0
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
$ cat go.mod
module example.com/hello

go 1.12

require (
    golang.org/x/text v0.3.0 // indirect
    rsc.io/quote v1.5.2
)
$

golang.org/x/text パッケージはタグ付けされた最新バージョン(v0.3.0)にアップグレードされ ました。go.mod ファイルも v0.3.0 を指定するように更新されています。indirect という コメントは、その依存関係がこのモジュールから直接使われているのではなく、他のモジュールの 依存関係を通じて間接的にのみ使われていることを示しています。詳細は go help modules を 参照してください。

次に rsc.io/sampler のマイナーバージョンをアップグレードしてみましょう。先ほどと同じように、 go get を実行してからテストを実行します。

$ go get rsc.io/sampler
go: finding rsc.io/sampler v1.99.99
go: downloading rsc.io/sampler v1.99.99
go: extracting rsc.io/sampler v1.99.99
$ go test
--- FAIL: TestHello (0.00s)
    hello_test.go:8: Hello() = "99 bottles of beer on the wall, 99 bottles of beer, ...", want "Hello, world."
FAIL
exit status 1
FAIL    example.com/hello   0.014s
$

おっと! このテストの失敗は、rsc.io/sampler の最新バージョンが私たちの使い方と互換性が ないことを示しています。このモジュールで利用可能なタグ付けされたバージョンを一覧表示して みましょう。

$ go list -m -versions rsc.io/sampler
rsc.io/sampler v1.0.0 v1.2.0 v1.2.1 v1.3.0 v1.3.1 v1.99.99
$

私たちはこれまで v1.3.0 を使っていました。v1.99.99 は明らかにだめなようです。代わりに v1.3.1 を試してみましょう。

$ go get rsc.io/sampler@v1.3.1
go: finding rsc.io/sampler v1.3.1
go: downloading rsc.io/sampler v1.3.1
go: extracting rsc.io/sampler v1.3.1
$ go test
PASS
ok      example.com/hello   0.022s
$

go get の引数の中で明示的に @v1.3.1 と指定していることに注目してください。一般に、 go get に渡す各引数には明示的なバージョンを指定できます。デフォルトは @latest で、これは 前述の定義に従って最新バージョンに解決されます。

新しいメジャーバージョンへの依存関係を追加する

パッケージに新しい関数を追加してみましょう。func Proverb はGoの並行性に関する格言を返します。 これはモジュール rsc.io/quote/v3 が提供する quote.Concurrency を呼び出すことで実現します。 まず hello.go を更新してこの新しい関数を追加します。

package hello

import (
    "rsc.io/quote"
    quoteV3 "rsc.io/quote/v3"
)

func Hello() string {
    return quote.Hello()
}

func Proverb() string {
    return quoteV3.Concurrency()
}

次に hello_test.go にテストを追加します。

func TestProverb(t *testing.T) {
    want := "Concurrency is not parallelism."
    if got := Proverb(); got != want {
        t.Errorf("Proverb() = %q, want %q", got, want)
    }
}

これでコードをテストできます。

$ go test
go: finding rsc.io/quote/v3 v3.1.0
go: downloading rsc.io/quote/v3 v3.1.0
go: extracting rsc.io/quote/v3 v3.1.0
PASS
ok      example.com/hello   0.024s
$

私たちのモジュールが rsc.io/quotersc.io/quote/v3 の両方に依存するようになったことに 注目してください。

$ go list -m rsc.io/q...
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
$

Goモジュールの異なるメジャーバージョン(v1v2、というように)はそれぞれ異なるモジュール パスを使います。v2 以降では、パスの末尾はそのメジャーバージョンで終わらなければなりません。 この例では、rsc.io/quotev3 はもはや rsc.io/quote ではなく、モジュールパス rsc.io/quote/v3 によって識別されます。この慣習は セマンティックインポートバージョニングと呼ばれ、 互換性のないパッケージ(メジャーバージョンが異なるもの)に異なる名前を与えます。対照的に、 rsc.io/quotev1.6.0v1.5.2 と後方互換性があるはずなので、rsc.io/quote という 名前を再利用します。(前節では、rsc.io/samplerv1.99.99rsc.io/samplerv1.3.0 と後方互換性があるはずでしたが、バグやモジュールの挙動に対するクライアント側の 誤った想定は、どちらも起こり得ることです。)

go コマンドは、1つのビルドに特定のモジュールパスのバージョンを最大でも1つしか含められません。 つまり、メジャーバージョンごとに最大1つ、rsc.io/quote を1つ、rsc.io/quote/v2 を1つ、 rsc.io/quote/v3 を1つ、というようになります。これによって、モジュールの作者は、1つの モジュールパスが重複しうるかどうかについて明確なルールを持てます。1つのプログラムが rsc.io/quote v1.5.2rsc.io/quote v1.6.0 の両方でビルドされることはあり得ません。 同時に、(パスが異なるため)モジュールの異なるメジャーバージョンを許容することで、モジュールの 利用者は新しいメジャーバージョンへ段階的にアップグレードできるようになります。この例では、 rsc/quote/v3 v3.1.0quote.Concurrency を使いたいものの、rsc.io/quote v1.5.2 の 使用箇所をまだ移行する準備ができていない、という状況でした。段階的に移行できる能力は、 大規模なプログラムやコードベースにおいて特に重要です。

依存関係を新しいメジャーバージョンにアップグレードする

rsc.io/quote の利用から rsc.io/quote/v3 のみの利用への移行を完了させましょう。メジャー バージョンが変わったことで、一部のAPIが削除されたり、名前が変わったり、その他互換性のない形で 変更されたりしていることが予想されます。ドキュメントを読むと、HelloHelloV3 になって いることがわかります。

$ go doc rsc.io/quote/v3
package quote // import "rsc.io/quote/v3"

Package quote collects pithy sayings.

func Concurrency() string
func GlassV3() string
func GoV3() string
func HelloV3() string
func OptV3() string
$

hello.go 内の quote.Hello() の使用を quoteV3.HelloV3() を使うように更新できます。

package hello

import quoteV3 "rsc.io/quote/v3"

func Hello() string {
    return quoteV3.HelloV3()
}

func Proverb() string {
    return quoteV3.Concurrency()
}

この時点で、名前を変えてインポートする必要はもうないので、それを元に戻せます。

package hello

import "rsc.io/quote/v3"

func Hello() string {
    return quote.HelloV3()
}

func Proverb() string {
    return quote.Concurrency()
}

すべてが正しく動いていることを確認するために、テストを再実行してみましょう。

$ go test
PASS
ok      example.com/hello       0.014s

使われていない依存関係を削除する

rsc.io/quote の使用箇所はすべて削除しましたが、go list -m all の出力や go.mod ファイル にはまだ表示されています。

$ go list -m all
example.com/hello
golang.org/x/text v0.3.0
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1
$ cat go.mod
module example.com/hello

go 1.12

require (
    golang.org/x/text v0.3.0 // indirect
    rsc.io/quote v1.5.2
    rsc.io/quote/v3 v3.0.0
    rsc.io/sampler v1.3.1 // indirect
)
$

なぜでしょうか? go buildgo test のような単一のパッケージのビルドでは、何かが不足していて 追加が必要かどうかは簡単に判断できますが、何かを安全に削除できるかどうかは判断できないから です。依存関係の削除は、モジュール内のすべてのパッケージと、それらのパッケージに対して考えられる すべてのビルドタグの組み合わせを確認した後でなければ行えません。通常のビルドコマンドはこの情報を 読み込まないため、依存関係を安全に削除できないのです。

go mod tidy コマンドは、これらの使われていない依存関係を整理してくれます。

$ go mod tidy
$ go list -m all
example.com/hello
golang.org/x/text v0.3.0
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1
$ cat go.mod
module example.com/hello

go 1.12

require (
    golang.org/x/text v0.3.0 // indirect
    rsc.io/quote/v3 v3.1.0
    rsc.io/sampler v1.3.1 // indirect
)

$ go test
PASS
ok      example.com/hello   0.020s
$

まとめ

Goモジュールは、Goにおける依存関係管理の未来です。モジュールの機能は、現在サポートされている すべてのGoのバージョン(つまりGo 1.11とGo 1.12)で利用できます。

この記事では、Goモジュールを使った次のようなワークフローを紹介しました。

  • go mod init は新しいモジュールを作成し、それを説明する go.mod ファイルを初期化します。
  • go buildgo test、その他のパッケージビルドコマンドは、必要に応じて新しい依存関係を go.mod に追加します。
  • go list -m all はカレントモジュールの依存関係を表示します。
  • go get は依存関係の必要バージョンを変更します(あるいは新しい依存関係を追加します)。
  • go mod tidy は使われていない依存関係を削除します。

ローカルでの開発でモジュールを使い始め、プロジェクトに go.modgo.sum ファイルを 追加することをおすすめします。フィードバックを提供し、Goにおける依存関係管理の未来を形作る 手助けをしていただくために、バグ報告experience reportをお送りください。

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

By Tyler Bui-Palsulich and Eno Compton