背景
go get でアップデートすると、なぜか別のライブラリがダウングレードする不思議な現象に出会いました。
$ go get github.com/userA/hoge go: downgraded github.com/userB/foo v1.2.1 => v1.0.0 go: downgraded github.com/userB/bar v1.10.0 => v1.1.0 go: upgraded github.com/userA/hoge v1.4.0 => v1.5.0
環境
Go v1.23.2
原因
原因は
- GoがMVS(Minimal version selection)を採用している
- ライブラリが循環参照している
ためでした。
Minimal version selectionとは
MVSはモジュールグラフ全体の依存関係を見て、各モジュールの最小(最も古い)の必要バージョンを使う仕組みです。

ref: https://go.dev/ref/mod#minimal-version-selection
この例で言うと、例えばDはD 1.3が最大バージョンですが、メインモジュールからすると最小の必要バージョンではありません。
なので
go get -uで全体をアップグレードgo get -u D@1.3して個別アップグレード
しない限りはD 1.2が選択されます。
メリット
- 選択されるバージョンが外部要因(=新しいパッケージのリリース)の影響を受けない
- 依存関係解決の再現性が保証される
- パッケージマネージャーの実装がシンプルになる
デメリット
- 最新バージョンになりにくい
- 場合によってはダウングレードする(今回のケース)
- 修正済みのバグを踏む可能性がある
循環参照の影響
上記の図ではライブラリ間の循環参照(相互参照)が無いため問題無いですが、個人ライブラリ等によっては循環参照のある依存関係が生まれているケースがあります。
この場合
A 1.5はB 1.4に依存している ↓ B 1.4はA 1.4に依存している ↓ A 1.4はB 1.3に依存してる ↓ B 1.3はA 1.3に依存してる ↓ ...
と巡り巡ってどんどん最低バージョンに遡っていくことが起きます。
解決方法
この状況の場合、ダウングレードしたライブラリをgo getで最新しようとしても、MVSによって循環参照を巡り巡って最低バージョンにされるので解決できません。
なので基本的には以下の方針となります。
- 循環参照を無くす
- go.modのreplaceでバージョンを固定する
a. 循環参照を無くす
根本解決はこちらになります。
循環参照はライブラリ間の結合を密にしますし、それによって意図しない影響が生まれるので基本的に避けるようにしましょう。
ただ長い間放置されたものだったりすると、修正コストが高くついたりするので緩和対応として次の方針を取ることも良いでしょう。
b. go.modのreplaceでバージョンを固定する
MVSはreplaceを考慮するのでダウングレードが発生しなくなります。

ref: https://go.dev/ref/mod#mvs-replace
replace github.com/userB/foo => github.com/userB/foo v1.2.1 replace github.com/userB/bar => github.com/userB/bar v1.10.0
その他
Maximal Version Selectionとは
Minimal version selectionの反対に、Maximal Version Selectionがあります。
これは
- 指定されたなかで最大の(=最新の)バージョンを使う。
- 最大バージョン選択は Bundler, npm 等をはじめとする多くのプログラミング言語のパッケージマネージャで採用されている。
例
v1.0 以上のバージョンが指定されていて現在の最新バージョンが v1.1 の場合、v1.1 がインストールされます。
まとめ
MVS(Minimal version selection)の説明と、それによって引き起こる期待しない挙動について説明しました。