“最小版本选择”(MVS)这个名字容易让人误会成“选最小的”。它的意思是:对每个模块,选需求里出现过的最大版本——这已经是满足所有需求的最小版本,不会自动升级到更新的发布。这篇用一个自己搭的本机模块仓库,把这条规则以及升级、降级、exclude、replace 的行为逐个跑出来。
b v1.1.0 需要:d v1.1.0
b v1.2.0 需要:d v1.3.0
c v1.1.0 需要:d v1.2.0
c v1.2.0 需要:d v1.4.0
d v1.1.0 需要:e v1.1.0
d v1.2.0 需要:e v1.1.0
d v1.3.0 需要:e v1.2.0
d v1.4.0 需要:e v1.2.0
e v1.1.0 需要:(无)
e v1.2.0 需要:(无)
e v1.3.0 需要:(无)
go mod tidy 之后的 go.mod:
module app
go 1.21
require (
b v1.2.0
c v1.2.0
)
require (
d v1.4.0 // indirect
e v1.2.0 // indirect
)
为了不依赖网络与别人的模块,我在本机用 file:// 形式的 GOPROXY 放了四个模块的多个版本(目录里是 Go 代理协议要求的 list、.info、.mod、.zip),并且让每个模块的代码打印自己的版本,这样运行时能看到最终链接了哪个版本。
go list -m all:
app
b v1.2.0
c v1.2.0
d v1.4.0
e v1.2.0
go mod graph:
app b@v1.2.0
app c@v1.2.0
app d@v1.4.0
app e@v1.2.0
app go@1.21
b@v1.2.0 d@v1.3.0
b@v1.2.0 go@1.21
c@v1.2.0 d@v1.4.0
c@v1.2.0 go@1.21
d@v1.4.0 e@v1.2.0
d@v1.4.0 go@1.21
e@v1.2.0 go@1.21
go@1.21 toolchain@go1.21
e 仓库里有的版本:e v1.1.0 v1.2.0 v1.3.0
go run . 输出(每个模块把自己实际用的版本打印出来):
b v1.2.0 → d v1.4.0 → e v1.2.0
c v1.2.0 → d v1.4.0 → e v1.2.0go list -m all 给出的是 MVS 的结果。b 虽然只声明需要 d v1.3.0,链接的却是 d v1.4.0,因为图里另一条边(c 要 d v1.4.0)要求更高。go mod graph 列出所有需求边,go mod tidy 把构建需要的间接依赖(d、e)也写进主模块的 go.mod 并标注 // indirect——这是为了让主模块不用下载整个传递闭包的 go.mod 就能算出构建列表(这一点是我对 go.mod 里列出间接依赖的目的的理解,没有单独验证)。
构建列表里 d 是 v1.4.0(b 要 v1.3.0、c 要 v1.4.0,取大者);e 是 v1.2.0(d v1.4.0 要 e v1.2.0)——即使 e 已经有 v1.3.0。 go list -m e → e v1.2.0
e 在仓库里已经有 v1.3.0,但没有任何需求指向它,所以选的是需求里出现过的最大版本 v1.2.0。不会因为“有新版本”就自动升级,也不会因为“别的模块更新了”而变——构建是可重复的,这是 MVS 的卖点。
go get e@latest:go: upgraded e v1.2.0 => v1.3.0
升级之后 go list -m all:
app
b v1.2.0
c v1.2.0
d v1.4.0
e v1.3.0
go.mod 里新增的需求:e v1.3.0 // indirect
go get d@v1.1.0(降级):
go: downgraded b v1.2.0 => v1.1.0
go: removed c v1.2.0
go: downgraded d v1.4.0 => v1.1.0
降级之后 go list -m all:
app
b v1.1.0
d v1.1.0
e v1.2.0
降级后用 -mod=readonly 构建(不允许自动改 go.mod):rc=1
main.go:6:2: cannot find module providing package c: import lookup disabled by -mod=readonly
降级后 go.mod:
module app
go 1.21
require b v1.1.0
require (
d v1.1.0 // indirect
e v1.2.0 // indirect
)要新版本必须显式 go get e@latest,go.mod 里多一行需求。降级更微妙:降 d 会让所有要求更高版本 d 的模块一起降(b 降到 v1.1.0)或被移除(c),因为不存在满足条件的更低版本。降级之后 e 留在 v1.2.0,不会自动跟着降。这个例子里主模块 import 了 c,被移除后构建失败(-mod=readonly 时直接报错)。
go mod why -m e:
# e
app
b
d
e
go.mod 里 exclude d v1.4.0 之后 go list -m all:
app
b v1.2.0
c v1.2.0
d v1.3.0
e v1.2.0
go: dropping requirement on excluded version d v1.4.0
replace e => ../e_local 之后 go run . 输出:
b v1.2.0 → d v1.4.0 → e 本地替换版
c v1.2.0 → d v1.4.0 → e 本地替换版go mod why -m 给出“主模块为什么需要它”的最短链;exclude 让某个版本被当作不存在,所有指向它的需求被丢弃,图里下一个满足的版本接上(这里是 d v1.3.0);replace 把一个模块整体换成本地目录,所有需求指向本地版本。replace 只对主模块的 go.mod 生效。