v2 go module
Go 里,v2+ 不是“同一个包的新版本”,而是“一个带新 import path 的新 module”。
也就是说:
v0: github.com/user/project // 不稳定版本,不加 /v0
v1: github.com/user/project // 稳定版本,也不加 /v1
v2: github.com/user/project/v2
v3: github.com/user/project/v3
从 v2 开始,主版本号必须进入 module path。
1. 为什么 v2+ 要改 import path
Go Modules 正式化了 Go 的一个重要规则:
- 如果旧 package 和新 package 使用相同 import path,
- 那么新 package 必须向后兼容旧 package。
这叫 import compatibility rule。
所以问题来了:
- v2 的定义就是:允许不兼容 v1。
那它就不能继续使用 v1 的 import path。
因此:
// v1
import "github.com/googleapis/gax-go"
// v2
import "github.com/googleapis/gax-go/v2"
对应的 go.mod 也要改:
module github.com/googleapis/gax-go/v2
一句话:
- 同一个 import path = 必须兼容;
- 不兼容 = 必须换 import path。
2. v2+ 的核心规则
从 v2 开始,module path 末尾必须带主版本号:
module github.com/user/project/v2
用户安装时:
go get github.com/user/project/v2@v2.0.0
用户代码里:
import "github.com/user/project/v2"
不能写成:
module github.com/user/project
然后打:
git tag v2.0.0
这样是不符合 Go Modules 规则的。
3. 为什么 Go 要这么设计
这是为了解决依赖冲突,尤其是 diamond dependency problem。
假设:
- 你的项目依赖 A 和 B
- A 依赖 github.com/user/lib v1
- B 依赖 github.com/user/lib v2
如果 v1 和 v2 的 import path 一样,就会冲突。
Go 的解决方式是让它们路径不同:
github.com/user/lib
github.com/user/lib/v2
于是同一个程序里可以同时存在:
import "github.com/user/lib"
import libv2 "github.com/user/lib/v2"
这不是重复,而是两个不同 module path。
核心好处:用户可以渐进迁移,不必一次性把整个项目从 v1 改到 v2。
4. 推荐的 v2+ 目录结构
文章推荐的方式是:在仓库里新建一个 v2/ 子目录。
例如:
github.com/googleapis/gax-go
/go.mod -> module github.com/googleapis/gax-go
/v2/go.mod -> module github.com/googleapis/gax-go/v2
也就是:
- v1 代码放根目录
- v2 代码放 v2/ 子目录
这样同一个仓库可以同时维护多个 major version。
示例结构:
project/
go.mod
hello.go
hello_test.go
v2/
go.mod
hello.go
hello_test.go
根目录 go.mod:
module github.com/user/project
v2/go.mod:
module github.com/user/project/v2
https://stackoverflow.com/a/70062338
Go 语言本身已采用主目录子目录方案, Ian Lance Taylor 在 “spec: language change review meeting minutes”:
Since the committee started Go has developed a standard way for handling incompatible Go library changes: a v2 release of the package.
The recent Go 1.22 release introduced the first incompatible change to the standard library, by adding math/rand/v2 as a replacement for math/rand.
(This is, of course, not really incompatible, as old programs continue to use math/rand and continue to work. Programs can update to the (slightly) incompatible math/rand/v2 at their leisure.)
在 src/math/rand 目录中可以看到 v2 子目录。
5. 创建 v2 module 的流程
假设已有 v1 module:
module github.com/user/project
创建 v2:
mkdir v2
cp *.go v2/
cp go.mod v2/go.mod
修改 v2/go.mod:
go mod edit -module github.com/user/project/v2 v2/go.mod
得到:
module github.com/user/project/v2 // 这个命令只改 v2/go.mod 的 module 名,不会改任何 Go 源码 import
然后进入 v2:
cd v2
go mod tidy
go test ./...
6. v2 内部 import 也要改
这是非常容易漏的一点。
如果 v2 module 里有多个 package,内部 import 也要从:
import "github.com/user/project/foo"
改成:
import "github.com/user/project/v2/foo"
否则会出现一个很隐蔽的问题:
v2 module 反过来依赖了 v1 module。
因为在 Go 看来:
github.com/user/project
github.com/user/project/v2
是两个不同 module。
所以迁移 v2 时,不只是改 go.mod,还要改所有内部 import。
可以批量替换:
find . -type f \
-name '*.go' \
-exec sed -i -e 's,github.com/user/project,github.com/user/project/v2,g' {} \;
实际项目中建议用 IDE、gofmt、测试和代码审查确认,不要盲目全局替换。
7. 发布 v2 预发布版本
如果 v2 API 还没最终稳定,可以先发预发布版本:
git tag v2.0.0-alpha.1
git push origin v2.0.0-alpha.1
用户可以显式安装:
go get github.com/user/project/v2@v2.0.0-alpha.1
预发布阶段可以继续调整 API。
注意:只要还没发布正式 v2.0.0,就还可以继续打磨 v2 API。
8. 发布正式 v2.0.0
当你确认 v2 API 稳定后:
cd v2
go mod tidy
go test ./...
cd ..
git add .
git commit -m "project: prepare v2.0.0"
git tag v2.0.0
git push origin main
git push origin v2.0.0
用户安装:
go get github.com/user/project/v2@v2.0.0
用户代码:
import "github.com/user/project/v2"
从这个时刻开始:
v2 也进入稳定承诺。
之后 v2.1.0、v2.2.0、v2.0.1 都应该保持 v2 内部向后兼容。
9. v1 和 v2 可以同时(也是必须)维护
发布 v2 后,不代表 v1 自动消失,v1 版本可能有用户在依赖,提出 issue。
你现在有两个 major version:
github.com/user/project v1.x.x
github.com/user/project/v2 v2.x.x
后续可能要同时维护:
v1.1.0 给 v1 加兼容功能
v1.0.1 给 v1 修 bug
v2.1.0 给 v2 加兼容功能
v2.0.1 给 v2 修 bug
这就是 v2 的真实成本:不是打一个 v2 tag 就完事,而是从此多维护一条稳定线。
所以文章强调:重大版本升级必须有充分理由。
不要因为 API 有点不顺眼就发 v2。
评论