Go modules: 依存関係とバージョン管理

1. 概要

Go 1.11以降、Go modulesは依存関係を管理するための公式メカニズムです。Go 1.16から、これがデフォルトモードになり、GOPATHに基づく古い方法に置き換わりました。このガイドは、モジュールのライフサイクル全体を説明します: 初期化、依存関係の追加、バージョンの選択とアップグレード、メジャーバージョン v2+ の処理、ローカルコードやプライベートリポジトリの操作です。読み終わった後、go.mod と go.sum が何を含んでいるかを理解します。また、Goが特定のバージョンを選択した理由と、一般的な依存関係エラーの修正方法を知ります。

2. モジュールの初期化

モジュールは、ルートに go.mod ファイルを持つディレクトリツリーです。go mod init を使用して新しいモジュールを作成します:

mkdir order-service && cd order-service
go mod init github.com/nonla/order-service

go.mod ファイルが生成されます:

module github.com/nonla/order-service

go 1.24

各行の意味は以下の通りです:

  • module はモジュールパスです。これは、モジュール内のすべてのパッケージのインポート接頭辞です。モジュールが他の人によって go get されるなら、このパスはリポジトリのURLと一致する必要があります。
  • go 1.24 はモジュールが要求する最小言語バージョンです。Go 1.21以降、この行は必須です。ツールチェーンが古い場合、Goは自動的に適切なツールチェーンをダウンロードします(GOTOOLCHAIN=auto の場合)またはエラーを報告します。

注意: 外部から決してインポートされないような内部アプリケーションの場合、order-service のような短いパスを使用できます。ただし、最初からURLスタイルのパスを使用すると、後でインポートを大量に変更する必要がなくなります。

3. 依存関係の追加

3.1. go get を使用する

注文のIDを生成するために UUID ライブラリが必要だとしましょう。github.com/google/uuid ライブラリを追加します:

go get github.com/google/uuid@v1.6.0

その後、ライブラリを使用するコードを書きます:

// order/order.go
package order

import "github.com/google/uuid"

type Order struct {
	ID       string
	Customer string
	Total    int64 // 単位: ドン
}

func NewOrder(customer string, total int64) Order {
	return Order{
		ID:       uuid.NewString(),
		Customer: customer,
		Total:    total,
	}
}

go.mod には require ブロックが追加されます:

module github.com/nonla/order-service

go 1.24

require github.com/google/uuid v1.6.0

go get ステップをスキップすることもできます: まずインポートを書き、その後 go mod tidy を実行します。Goが自動的に最新バージョンを見つけて go.mod に書き込みます。

3.2. go.sum ファイル

同時に、Goが go.sum ファイルを作成します:

github.com/google/uuid v1.6.0 h1:<モジュール内容のハッシュ>
github.com/google/uuid v1.6.0/go.mod h1:<go.modファイルのハッシュ>

各依存関係には2行があります。最初の行はモジュール全体のハッシュで、次の行はそれの go.mod ファイルのハッシュです。モジュールをダウンロードするとき、Goは実際のハッシュを go.sum と比較します。また、公開チェックサムデータベース(sum.golang.org)と照合します。誰かが既に発行されたタグのコンテンツを変更した場合、ビルドは失敗します。見知らぬコードを無言で使用することはありません。

注意: go.mod と go.sum の両方を常にコミットしてください。go.sum を .gitignore に追加しないでください。このファイルがない場合、CI マシンは依存関係を検証できません。

3.3. 直接依存と間接依存

テストを書くために testify を追加します:

go get github.com/stretchr/testify@v1.10.0

go.mod には2つの require ブロックがあります:

require (
	github.com/google/uuid v1.6.0
	github.com/stretchr/testify v1.10.0
)

require (
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)

// indirect コメントは、コードが直接インポートしないが、別の依存関係に必要なモジュールをマークします。Goがビルドを完全に再現できるようにするため、これを go.mod に書き込みます。そのため、Goは各依存関係の go.mod を再度読む必要がありません。依存関係グラフを構築するために必要です。

4. バージョンと Goがバージョンを選択する方法

4.1. セマンティックバージョニング

Go modules はセマンティックバージョニング形式 vMAJOR.MINOR.PATCH を使用します:

  • PATCH (v1.6.0 → v1.6.1): バグ修正、API は変わりません。
  • MINOR (v1.6.0 → v1.7.0): 機能追加、後方互換性を維持。
  • MAJOR (v1 → v2): 互換性が破れる変更。

バージョンは v プレフィックス付きの git タグである必要があります。タグなしのコミットを指す場合、Goは疑似バージョンを作成します:

go get github.com/google/uuid@0f3b7a2
github.com/google/uuid v1.6.1-0.20240815101530-0f3b7a2c1d4e

疑似バージョンは3つの部分で構成されます: ベースバージョン、コミットのUTCタイムスタンプ、ハッシュの最初の12文字。これにより、疑似バージョンは時間順に正しくソートされます。

4.2. Minimal Version Selection

これは npm や Maven との最大の違いです。GoはMinimal Version Selection (MVS)アルゴリズムを使用します。各モジュールについて、Goは要求されたバージョン間の最高バージョンを選択します。存在する最新バージョンではなく。

例えば、プロジェクトに2つの依存関係があります:

order-service
├── payment-sdk  → require github.com/google/uuid v1.3.0
└── shipping-sdk → require github.com/google/uuid v1.5.0

uuid v1.6.0 が既にリリースされていても、Goは v1.5.0 を選択します。理由: これはすべての要件を満たす最低バージョンです。結果:

  • ビルドは再現可能です。ライブラリが新しいバージョンをリリースしても、ビルドは自動的には変わりません。
  • 別のロックファイルは不要です。go.mod だけで十分です。
  • より新しいバージョンを使用するには、明示的にアップグレードする必要があります。

実際に選択されたバージョンを見るには:

go list -m all
github.com/nonla/order-service
github.com/davecgh/go-spew v1.1.1
github.com/google/uuid v1.5.0
...

4.3. アップグレードとダウングレード

新しいバージョンがある依存関係を確認します:

go list -m -u all
github.com/google/uuid v1.5.0 [v1.6.0]
github.com/stretchr/testify v1.10.0

括弧内のバージョンはアップグレード可能な最新バージョンです。よく使用されるアップグレードコマンド:

go get github.com/google/uuid@latest   # 最新バージョン
go get github.com/google/uuid@v1.5.0   # 特定のバージョンに(ダウングレード可能)
go get -u=patch ./...                  # すべての依存関係の PATCH のみアップグレード
go get -u ./...                        # すべての依存関係の MINOR + PATCH をアップグレード
go get github.com/google/uuid@none     # 依存関係を完全に削除

注意: go get -u ./... は間接依存もアップグレードします。差分は非常に大きくなる可能性があります。本番プロジェクトでは、各モジュールをアップグレードし、テストを実行してからコミットしてください。

5. メジャーバージョン v2 以上

Goはインポート互換性ルールを適用します: 同じインポートパスを持つ2つのバージョンは後方互換である必要があります。したがって、v2以降では、メジャーバージョンはモジュールパスの一部です。

ルーターの例 chi:

go get github.com/go-chi/chi/v5@v5.1.0
package main

import (
	"net/http"

	"github.com/go-chi/chi/v5"
)

func main() {
	r := chi.NewRouter()
	r.Get("/orders", func(w http.ResponseWriter, _ *http.Request) {
		w.Write([]byte("注文一覧"))
	})
	http.ListenAndServe(":8080", r)
}

/v5 サフィックスはインポートパスに含まれます。パッケージ名は chi のままなので、通常どおり chi.NewRouter() を呼び出します。chi/v4 と chi/v5 は異なるパスを持つため、両方が同じバイナリに共存できます。これにより、段階的に移行できます。

ライブラリの作成者で v2 をリリースしたい場合:

module github.com/nonla/pricing/v2

go 1.24

module 行を /v2 サフィックス付きのパスに変更し、内部インポートを更新してから v2.0.0 タグを付けます。

注意: よくあるエラーは v2.0.0 タグを付けるだけでモジュールパスを忘れることです。その場合、go get github.com/nonla/pricing@v2.0.0 は invalid version: module contains a go.mod file, so module path must match major version というエラーを報告します。

6. 便利な go mod コマンド

go mod tidy      # 不足しているモジュールを追加し、未使用のモジュールを削除
go mod why -m gopkg.in/yaml.v3   # このモジュールがなぜ存在するのか
go mod graph     # 全体の依存グラフをプリント
go mod verify    # ローカルキャッシュが go.sum と一致しているか確認
go mod download  # 依存関係を事前ダウンロード (Docker レイヤーに便利)

go mod why の出力例:

# gopkg.in/yaml.v3
github.com/nonla/order-service/order
github.com/stretchr/testify/assert
gopkg.in/yaml.v3

上から読む: パッケージ order(テスト内)は testify/assert をインポートし、testify/assert は yaml.v3 をインポートします。

コミット前に毎回 go mod tidy を実行してください。CI では、go.mod がすでに整理されているか確認できます:

go mod tidy -diff

Go 1.23以降、-diff フラグは差分を出力し、ファイルがまだ tidy されていない場合は0以外の終了コードを返します。このコマンドはファイルを上書きしません。

7. ローカルコードの操作: replace と go.work

7.1. replace ディレクティブ

隣接のディレクトリで pricing ライブラリを修正していて、order-service がそれを直ちに使用したい場合を想像してください:

require github.com/nonla/pricing v1.2.0

replace github.com/nonla/pricing => ../pricing

Goはネットワークからダウンロードする代わりに ../pricing からコードを読みます。replace は fork を指すこともできます:

replace github.com/nonla/pricing => github.com/minh/pricing v1.2.1-fix

注意: replace はメインモジュールでのみ有効です。モジュールがライブラリの場合、それを使用する人は replace を受け取りません。さらに、replace をローカルパスに指すようにコミットしないでください。CI でのビルドは ../pricing ディレクトリが存在しないため失敗します。

7.2. go.work でのワークスペース

Go 1.18以降、ワークスペースは複数モジュールを同時に開発するより清潔な方法です:

go work init ./order-service ./pricing

go.work ファイルが生成されます:

go 1.24

use (
	./order-service
	./pricing
)

ワークスペース内で go build または go test を実行するとき、github.com/nonla/pricing へのすべてのインポートはローカルコードを使用します。go.mod を変更する必要はありません。通常、go.work はコミットされず、各開発者が自分のディレクトリ構成に従って作成します。

7.3. exclude と retract

exclude はメインモジュールで特定のバージョンを除外するために使用されます:

exclude github.com/google/uuid v1.4.0

retract はライブラリの作成者が宣言して、リリース済みのバージョンを使用しないでくださいと報告します:

retract v1.2.0 // 国際注文のVAT計算が誤っていました

この行を含む新しいバージョンをリリースした後、go get @latest は v1.2.0 をスキップします。go list -m -u コマンドも、それを使用している人に警告を出します。

8. プライベートモジュールとプロキシ

デフォルトでは、Goは GOPROXY=https://proxy.golang.org,direct を通じてモジュールをダウンロードし、sum.golang.org を通じてチェックサムを確認します。内部リポジトリの場合、公開プロキシはアクセスできず、モジュール名を外部に公開したくもありません。宣言してください:

go env -w GOPRIVATE=github.com/nonla/*,gitlab.nonla.internal/*

GOPRIVATE のパターンと一致するモジュールは git 経由で直接ダウンロードされ、チェックサムデータベースをスキップします。Git の認証を設定する必要があります。例えば ~/.netrc または SSH:

git config --global url."git@github.com:".insteadOf "https://github.com/"

企業が独自のプロキシ(Athens、Artifactory等)を持っている場合、GOPROXY の先頭にそのプロキシを配置します:

go env -w GOPROXY=https://goproxy.nonla.internal,https://proxy.golang.org,direct

9. 依存関係によるテスト動作

セクション 3.3 で追加した testify を使用して NewOrder のテストを作成します:

// order/order_test.go
package order

import (
	"testing"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestNewOrder_GeneratesValidUniqueID(t *testing.T) {
	first := NewOrder("Nguyễn Văn An", 250_000)
	second := NewOrder("Trần Thị Bình", 120_000)

	_, err := uuid.Parse(first.ID)
	require.NoError(t, err)

	assert.NotEqual(t, first.ID, second.ID)
	assert.Equal(t, "Nguyễn Văn An", first.Customer)
	assert.EqualValues(t, 250_000, first.Total)
}

テストを実行します:

go test ./...
ok  	github.com/nonla/order-service/order	0.004s

テストは3つのことを確認します: ID は有効な UUID、2つの注文は異なるID、その他のフィールドは正しく割り当てられています。require.NoError は ID がパースできない場合、テストを即座に停止します。assert はエラーを記録してテスト実行を続けます。新しいマシンで初めて実行するとき、Goは go.mod の正確なバージョンに従って依存関係をダウンロードし、go.sum でハッシュを確認してからコンパイルします。

10. よくあるエラー

  • missing go.sum entry for module providing package: go.sum に対応する行がありません。通常、誰かが go.mod を手動で編集したためです。修正方法: go mod tidy を実行します。
  • checksum mismatch: モジュールコンテンツがハッシュと一致しません。go clean -modcache を試してください。公開モジュールでも引き続き問題がある場合、これは重大な兆候です。通過するために go.sum の行を削除しないでください。
  • go: updates to go.mod needed: CI は通常 -mod=readonly で実行されますが、go.mod はまだ tidy されていません。ローカルで go mod tidy を実行してからコミットし直してください。
  • メジャーバージョンのインポート間違い: .../chi の代わりに github.com/go-chi/chi/v5 を使用しましょう。古い v1 バージョンまたは見知らぬ疑似バージョンが表示されます。まずインポートパスを確認してください。
  • vendor/ の過度な使用: go mod vendor はオフライン構築が完全に必要な場合に有用です。それ以外は、プロキシと go.sum の組み合わせで再現可能性が保証されるため、数千の vendor ファイルをコミットする必要はありません。

11. まとめ

Go modules は依存関係の問題をいくつかの単純なアイデアで解決します。go.mod は要件を宣言し、go.sum は整合性を保証し、Minimal Version Selection はロックファイル不要で再現可能なビルドを実現します。メジャーバージョンはインポートパスの一部なので、互換性が破れる変更は常にコードに明確に表示されます。

日々の仕事では、4つの習慣を覚えておくだけです:

  • go get module@version を使用して、意図的に依存関係を追加またはアップグレードします。
  • コミット前に go mod tidy を実行します。
  • 複数モジュールを同時に開発する場合は、replace の代わりに go.work を使用します。
  • 内部リポジトリに対して GOPRIVATE を設定します。
golanggo modulesdependencyversioning