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を設定します。