/v1 和 /v2 路径前缀是唯一推荐方案,其他方式在缓存、日志、网关、调试四环节易出错;必须用 Group/Mount/Subrouter 隔离路由并定义独立 DTO struct,禁用指针模拟兼容,X-API-Version 仅作兜底。

/v1 和 /v2 路径前缀是唯一值得投入的方案,其他方式都会在缓存、日志、网关、调试四个环节翻车。
用 Group() 或 Subrouter() 隔离路由,别手动解析路径
你不需要在 handler 里写 strings.HasPrefix(r.URL.Path, "/v2"),那会让路由树不可见、中间件无法按版本启用、Prometheus 指标聚合失效。
-
gin:必须用r.Group("/v1"),而不是r.GET("/v1/users", ...)—— 后者丢失分组语义,v1.Use()中间件加不上 -
chi:必须用r.Mount("/v2", v2Router),Mount不继承父中间件,避免 v1 的日志中间件意外跑到 v2 里 -
gorilla/mux:用r.PathPrefix("/v1").Subrouter(),注册后r.URL.Path是/users,不是/v1/users,不用再切字符串
每个版本必须定义独立的 DTO struct,哪怕字段一模一样
共用 User 结构体是高频崩溃点:v2 加个 Nickname string 字段,v1 客户端 JSON 解析直接报错;v1 的 omitempty tag 在 v2 新字段为零值时,可能让旧字段意外消失。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 声明
UserV1和UserV2两个 struct,即使当前字段完全一致 - 禁止用指针字段“模拟兼容”,比如把
Name string改成Name *string来兼容旧客户端——这只会掩盖字段语义变化 - 数据库 model(如 GORM struct)保持稳定,只做存储;API 层靠手动赋值或
mapstructure.Decode做 domain → DTO 转换
X-API-Version 头只适合兜底,且必须由中间件注入 context
它不是替代方案,而是给老 SDK 或无法改 URL 的客户端留的逃生通道。一旦当成主力方案,curl 测试要手动加 header、浏览器直访失效、CDN 缓存策略难配置、HTTP/2 代理还可能丢 header。
- 中间件中校验值是否合法:
v1、v2、v3之外的版本必须返回400 Bad Request - 解析结果必须通过
context.WithValue(ctx, versionKey, v)注入,后续 handler 从ctx.Value()取,别重复解析 - 绝不允许同时启用路径 + header 两种方式又不定义优先级——
/v1/users和X-API-Version: v2冲突时行为不可控
components/schemas 必须按版本拆开,否则 @Success 200 {object} UserV1 注释会混进 v2 文档;Prometheus 的 http_request_duration_seconds{path="/users"} 会把所有版本流量揉在一起,查慢请求时根本分不清是哪个版本拖慢的。

















