Go API 版本升级必须显式隔离路由、包路径与结构体,使用 router.Group("/v1")/router.Group("/v2") 前缀,严格禁止混用 handler 和 struct,字段变更需零感知兼容,模块大版本升级须修改 go.mod 路径为 /v2。

Go 没有自动版本路由或隐式兼容机制,API 升级必须显式拆分、独立维护、手动校验;不隔离 handler、不分离包路径、不校验结构体字段变更,90% 的“升级”会在上线后触发 silent panic 或数据丢失。
用 router.Group("/v1") 和 router.Group("/v2") 隔离路由
路径前缀是唯一能被 CDN、Nginx、浏览器地址栏、OpenAPI 工具直接识别的版本标识。别试图用 query 或 header 替代——它们无法参与缓存键计算,也绕不开中间件作用域混乱问题。
-
router.Group("/v1")下注册的所有 handler 必须只引用handlers/v1包,不能 importhandlers/v2或混用 struct - 每个 group 应挂载独立中间件:比如
v2.Use(authzMiddleware),而v1仍走旧鉴权逻辑 - 避免写
r.Get("/v1/users", ...)这种扁平路由——它破坏子路由树,导致v1.Use()失效,且无法批量启用/禁用某版本 - 如果用 chi,必须用
r.Mount("/v2", v2Router)而非r.Route("/v2", ...),否则中间件链不生效
struct 字段变更必须零感知兼容,不能靠“测试一下”赌运气
JSON 序列化是接口契约的核心,json: tag 的写法错误会导致旧客户端解析失败或静默丢字段。这不是运行时能 catch 的错误,而是编译通过但线上出问题。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 新增字段一律加
json:",omitempty",并设合理零值(string默认"",int默认0) - 废弃字段不要删,改成未导出字段:
oldName string `json:"old_name,omitempty"`,加注释// deprecated: use FullName instead - 重命名字段时双写:
Name string `json:"name"`和FullName string `json:"full_name,omitempty"`,再实现UnmarshalJSON做兼容映射 - 敏感字段升级(如
Password→PasswordHash),旧 struct 中保留Password字段但始终为空字符串,确保反序列化不 panic
模块大版本升级必须改 go.mod 路径,replace 不是升级方案
把 module example.com/api 升到 v2,不改路径就等于没升级——go build 会直接报 invalid version: module contains a go.mod file, so major version must be compatible: should be v0 or v1, not v2。
立即学习“go语言免费学习笔记(深入)”;
- 新
go.mod必须写成module example.com/api/v2,所有导入语句同步改为import "example.com/api/v2" -
replace只能在本地开发阶段临时使用,CI 流程中必须加检查:grep -q "replace" go.mod && exit 1 - Git 分支建议单独建
v2,避免和main(v1)分支互相污染;不要在main上开feature/v2分支 - 动态构造包路径的代码(如
plugin.Open("example.com/api/" + version)或配置驱动加载)必须显式适配/v2后缀,否则运行时 panic
最易被忽略的是 handler 内部对 context 或 error 的隐式依赖:比如 v1 handler 中调用 c.MustGet("user_id").(int),v2 改成 string 类型但没改所有调用点,就会在某个深埋的 service 层 panic。这类问题不会出现在单元测试里,只会在特定请求路径上爆发。

















