Goは手続き型言語なのにモジュール対応してしまった。
これが悲劇の始まりであり、go.modの宿命でもある。
本記事は、go.mod/tidy/replace の関係を 筆者が1日でざっくり理解した内容 を整理した記録です。
筆者は独自に Real言語 という、Goを基盤にした新しいプログラミング言語を設計していますが、その過程で見えたことを共有することが本記事の目的です。
理論よりも現実、哲学よりも再現性。
でも、なぜ壊れるのか、その構造もちゃんと見ていきます。
1. go.modとは何なのか
Goにおける go.mod は単なる設定ファイルではなく、
プロジェクト構造を宣言する設計図 です。
多くの利用者は go.mod を「依存を記録するファイル」だと思いがちですが、実際には「構造を宣言するファイル」です。
module example.com/hello
go 1.22
require (
github.com/foo/bar v1.0.0
)
-
moduleは「このディレクトリ以下が1つの世界です」という宣言です。 - Goはここを基準に import を辿り、依存関係を構築します。
- つまり、適切なimport宣言を記述していくには、go.modファイルの内容を正確に理解・把握することが求められます。
- よって go.mod は「依存ファイル」ではなく 構造宣言ファイル です。
go.sum はその裏で、依存の署名やハッシュを保持しているだけの存在です。
2. go.modが何を解決できるのか/何を解決するための存在なのか
Goのモジュール導入前は、GOPATH 配下で全プロジェクトが依存を共有しており、
バージョン競合やビルド再現性の問題がありました。
とはいえ、Goがこれを導入したのは「外部ライブラリを安全に共有したい」という実務的必然でもありました。
つまり、哲学と現実のバランスを取ろうとした結果として生まれたのが go.mod なのです。
go.mod はこれを解決するために生まれました。
目的は次の3つです:
- 依存のバージョン固定
- プロジェクト構造の独立
- 再現可能なビルド
ただし、Goはもともと 手続き型=内部完結型 の言語です。
そこに「外部依存の管理」という新しい層を後付けしたことで、構造的なねじれが生じました。
これが後の「go.modが壊れる」問題の根源です。
3. go mod tidy コマンドの正体
go mod tidy は、プロジェクト全体を再走査して、go.mod と go.sum を再構築 するコマンドです。
go mod tidy
go mod tidy は、一見すると「整理整頓ボタン」で、プロジェクトの状態を最適化してくれるように見えます。
しかし実際には、全構造を“正しいと思い込んで”再構築する再帰的ツールです。
これは現代のVibeコーディングにも通ずる危うさを含んでいます。
(※Vibeコーディング=AIや補助ツールに任せすぎることで、構造理解を失う危険を指しています。)
go mod tidyが内部でやっていること:
- import文をすべて解析
- 不足している依存を追加
- 不要な依存を削除
- go.sum を再計算
つまり tidy は 構造の再整合ツール です。
構造的に言えば「自分の全体像を再構築する再帰処理」です。
mainパッケージを起点に、全てのimportを走査した上でgo.modを更新してくれるため、一見便利なコマンドに感じますが、ここに大きなリスクが潜んでいます。
一見便利な tidy ですが、Go への構造理解が浅いまま実行すると、思わぬ“構造破損”を引き起こします。
次章では、その典型例を見ていきます。
4. go mod tidy コマンドが破損をもたらす時
tidy は「整合」を目的としているため、
構造が不安定な状態で実行すると 破壊的な再構築 を行います。
破損が起きやすい例:
- 下層フォルダにも go.mod が存在する
- import が外部モジュールをまたいでいる
- モジュールキャッシュが古い
- vendor と go.sum の不一致
こうした場合、tidy は「正しい状態を知らないまま正しさを上書き」します。
結果として依存が壊れたり、ビルドが通らなくなったりします。
tidy は賢い整合ツールではなく、“何も疑わず全構造を再帰的に整える装置”です。
便利に見えますが、上記のような破損が起きやすい例に該当する環境で使うと、高確率で破壊的な変更をもたらします。
5. go mod replace の存在意義と使い道
replace は、tidy で壊れた構造を手動で修復するための手段です。
tidy によって破損することを予想できているのであれば、破損を事前に回避するための予防手段でもあります。
replace example.com/foo => ../foo
これで依存をローカルディレクトリなどに差し替えられます。
便利ですが、「恒久的な修復手段」ではありません。
当然go mod tidyの実行で再度破損します。
- 用途:ローカルでの依存テスト、キャッシュ破損時の応急処置
- リスク:replace が増えると構造が歪み、tidy の再整合が効かなくなる
replace は「壊れた構造を立て直す仮設の手段」ですが、放置すると建物が歪むように、モジュールも歪みます。
replace を必要とする環境になっているのなら、その環境は既に赤信号の状態にあると言えます。
6. 最後の手動編集
最終手段として、go.mod や go.sum を 直接編集することは悪ではありません。
# キャッシュを消して再整合
go clean -modcache
rm go.sum
go mod tidy
- 不要な require は手動で削除
- replace の依存を整理
- 必要に応じて go.sum を再生成
go.mod は構造の設計図。
tidy は再帰整合。
replace は応急処置。
これさえ理解していれば、壊れてもプロジェクトを修復することが可能になります。
また、あらかじめ理解しておくことで、プロジェクトを壊れにくくすることができます。
結論
Goは「手続き型言語なのにモジュール対応してしまった」。
この矛盾が、tidyやreplaceが必要になるすべての理由です。
でも、その構造を理解してしまえば――
tidyの挙動も、replaceの意味も、
そして壊れた時の修復も、すべて説明がつきます。
理解して使えば、Go Modules はもう怖くない。