最稳做法是用路径前缀(如/v1)注册独立路由组,严禁复用handler或共享DTO;query参数和header方式会导致缓存失效、日志混淆、文档生成困难及调试不可见。

直接用路径前缀(如 /v1)注册独立路由组,别试图复用 handler 或共享 DTO 结构体——这是最稳、最易维护、也最不容易翻车的做法。
为什么不能把版本号塞进 query 参数或 header
query 参数(如 ?version=v1)会让缓存失效、日志聚合失真、OpenAPI 文档无法按版本生成;X-API-Version 或 Accept header 则导致 curl 测试麻烦、浏览器直访不可用、CDN 缓存策略难配置。更关键的是:路由树在 router.Walk() 里完全不可见,调试时你根本不知道请求到底进了哪条路。
- curl
https://api.example.com/users?version=v1和https://api.example.com/users?version=v2在 Nginx 日志里都记作/users,查慢请求时没法区分 - Prometheus 的
http_request_duration_seconds{path="/users"}指标会把 v1/v2 混在一起,报警阈值失去意义 - Swagger UI 无法为不同版本生成独立的
components/schemas,字段差异全糊成一团
gorilla/mux 或 chi 中正确注册 v1 路由
用 PathPrefix("/v1") 创建子路由器,所有 v1 接口都挂在这下面。v1 和 v2 的 handler 可以共用 service 层,但 request/response 结构必须物理隔离。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 定义独立 DTO:比如
V1User和V2User,即使字段名和类型完全一样,也要分开声明 - handler 末尾显式调用
json.Marshal(v1User),不传 domain model 直接 encode - v1 的 struct 不要依赖
omitempty隐式过滤字段——v2 新增字段设为零值时,v1 响应可能意外漏掉旧字段 - 别在中间件里解析
r.URL.Path提取版本再跳转,这会让路由逻辑脱离框架管控
如何安全下线 v1
下线不是删代码,而是加约束和可观测性。v1 接口停用前至少保留 6 个月,并配合明确的弃用策略。
立即学习“go语言免费学习笔记(深入)”;
- 响应头加
Deprecation: true和Link: <https:>; rel="successor-version"</https:> - 在 Prometheus 中单独打标:给 v1 请求加
api_version="v1"标签,监控调用量衰减曲线 - 日志里统一记录
v1_deprecated字段,方便 ELK 聚合分析剩余客户端分布 - 真正删除前,先在 CI 中禁用 v1 的测试用例,确认无新 PR 引用 v1 DTO 或 handler
最常被忽略的一点:v1 的 DTO 字段哪怕和 v2 完全一致,也不能用 type alias 或嵌套共用结构体——Go 的 json tag 是包级生效的,一个字段改了 tag(比如加了 json:"full_name"),v1 响应就可能突然多出字段或重命名,老前端当场 panic。

















