Goブログ

Goモジュールを使う

Tyler Bui-Palsulich および Eno Compton
2019年3月19日

はじめに

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

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

Go 1.11および1.12には、モジュールのサポート、依存関係のバージョン情報を明示的にし、管理を容易にするGoの新しい依存関係管理システムが含まれています。このブログ投稿では、モジュールを使い始めるために必要な基本的な操作を紹介します。

モジュールは、ルートにgo.modファイルがあるファイルツリーに保存されたGoパッケージのコレクションです。go.modファイルは、モジュールのモジュールパス(ルートディレクトリに使用されるインポートパスでもある)と、ビルドを成功させるために必要な他のモジュールである依存関係の要件を定義します。各依存関係の要件は、モジュールパスと特定のセマンティックバージョンとして記述されます。

Go 1.11の時点では、現在のディレクトリまたは親ディレクトリにgo.modがあり、ディレクトリが$GOPATH/srcにある場合、goコマンドはモジュールの使用を有効にします。($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/worldexample.com/helloモジュールの一部として自動的に認識されます。

依存関係の追加

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内のどのモジュールでも提供されていないパッケージのimportを検出すると、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のブログ投稿「ソフトウェアの依存関係の問題」を参照してください。

上記で見たように、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は、モジュールrsc.io/quote/v3によって提供されるquote.Concurrencyを呼び出すことによって、Go並行性に関する格言を返します。まず、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/sampler v1.99.99rsc.io/sampler v1.3.0と後方互換性があるはずでしたが、バグやモジュールの動作に関するクライアントの誤った仮定はどちらも起こりえます。)

goコマンドは、ビルドに特定のモジュールパスのバージョンを最大1つだけ含めることを許可します。つまり、各メジャーバージョンのうち最大1つです。つまり、rsc.io/quotersc.io/quote/v2rsc.io/quote/v3などがそれぞれ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 allgo.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.modおよびgo.sumファイルを追加することをお勧めします。フィードバックを提供し、Goでの依存関係管理の将来を形作るために、バグレポートまたは経験レポートをお送りください。

モジュールの改善にご協力いただき、ありがとうございます。

次の記事: Go 1.12でデプロイしたものをデバッグする
前の記事: 新しいGo開発者ネットワーク
ブログインデックス