移动端接口版本兼容需通过r.group()明确版本边界,dto按版本隔离,下线前返回410 gone并监控存量请求。

移动端接口版本兼容,本质是让新旧客户端能同时跑通、不互相干扰。Gin 本身不提供版本管理能力,得靠路由组织 + 结构体隔离 + 显式转换来兜住——不是加个中间件就能解决的事。
为什么必须用 r.Group() 而不是手拼路径
硬写 r.GET("/v1/users", handler) 看似省事,但实际会埋三个坑:
- 路由不可见:
router.Walk()扫不到真实分组结构,OpenAPI 工具生成文档时漏掉版本上下文,Prometheus metrics 无法按/v1、/v2自动打标 - 中间件失控:比如 v2 新增了设备指纹校验,你没法只给
/v2加中间件,只能在每个 handler 里重复写if version == "v2" - 反向代理失效:Nginx 的
location /v1/规则匹配不上/users/v1这种路径,导致网关转发错乱
正确做法是让 Gin 明确知道“这是个版本边界”:
v1 := r.Group("/v1")
v1.GET("/users", getUserV1)
v1.POST("/users", createUserV1)
v2 := r.Group("/v2")
v2.GET("/users", getUserV2)
v2.POST("/users", createUserV2)
/v1/users 还是 /api/v1/users?路径设计的两个硬约束
版本段必须是路径第一级,且不能冗余嵌套。常见错误包括:
-
/api/v1/users:如果整个服务只暴露 API,/api是多余前缀,徒增维护成本 -
/v1/api/users或/users/v1:破坏 REST 资源语义——/users是资源,版本是访问维度,不该混进资源路径 -
/v1/users?version=v2:查询参数不参与路由匹配,中间件拦不住,CDN 缓存还会把 v1/v2 响应混存
真正有效的结构只有两种:
-
/v1/users(推荐) -
/v1/users/:id/orders(嵌套资源走子 Group,不是拼字符串)
DTO 必须按版本拆开,别靠 json:"name,omitempty" 蒙混过关
v2 新增字段,v1 客户端解析失败不是 bug,是契约破坏。Go struct 的 omitempty 无法表达“v1 不返回该字段,v2 必须返回”的语义。
正确做法:
- 为每个版本定义独立 DTO:
UserRespV1、UserRespV2,即使字段名一样也分开声明 - handler 只调用领域层获取原始数据:
userDomain := svc.GetUser(id) - 再交由版本专用函数转换:
c.JSON(200, toResponseV1(userDomain))或c.JSON(200, toResponseV2(userDomain))
这样既能复用业务逻辑,又避免 JSON 序列化时字段泄露或缺失。
v1 下线时容易忽略的监控和状态码细节
很多团队直接删掉 v1 路由,结果线上还在跑的老版本 App 突然全量报错。真正的下线动作要分三步:
- 先返回
410 Gone(而非404),明确告诉客户端“这个版本已永久移除” - 在中间件里统计
/v1/*的请求量和 User-Agent,确认还有多少存量用户 - 真要彻底下线,用
301 Moved Permanently指向 v2,并附带迁移说明(比如返回{"message": "请升级至新版App"})
最隐蔽的问题是:如果你把鉴权中间件只挂到了 v2 组里,v1 就可能裸奔——跨版本通用中间件(如日志、trace ID 注入)必须注册在根 r 上,或者显式传入每个 Group。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











