Gin中安全支持v1/v2需路由隔离、中间件按需注入、响应结构体分离;先声明Group再注册子路由;用版本专属Handler和响应结构体;通用中间件挂根路由,版本特有中间件挂对应Group;v1下线用410兜底。

要在Gin框架中安全、可维护地支持v1和v2两个API版本,必须让不同版本的路由完全隔离、中间件按需注入、响应结构体严格分离,否则旧客户端会因字段变更或行为差异突然报错。
创建独立的v1和v2路由组
先调用r.Group("/v1")和r.Group("/v2")分别声明两个顶层分组,【必须在注册任何子路由前完成分组声明】;如果先写r.GET("/v1/users", ...)再建v1 := r.Group("/v1"),会导致后续v1.GET("/users", ...)永远不生效。
把v1和v2的子路由全部写在对应分组的{}代码块内,这是Gin官方推荐的写法,能从语法层面防止路由错挂到根路由上。
路径前缀必须是第一级,禁止写成/api/v1或/v1/api——前者冗余,后者破坏REST资源语义,/users作为资源路径应稳定,版本只是访问维度。
为不同版本绑定专属Handler函数
方法一:直接定义带版本后缀的函数名,如GetUsersV1(c *gin.Context)和GetUsersV2(c *gin.Context)。这样命名清晰,IDE能精准跳转,Git Diff时也容易看出逻辑变更点。
方法二:共用一个入口函数,内部用c.Request.URL.Path或上下文键判断当前版本,再分支调用不同逻辑。但这种方式绕过了编译期检查,容易遗漏v2新增字段的兼容处理,不推荐。
注意:不要复用同一个结构体做JSON返回,哪怕字段名一致也要定义UserRespV1和UserRespV2两个类型——v2加了role字段后,v1客户端解析含该字段的响应可能panic。
中间件挂载位置必须精确控制
第一步:将日志、TraceID注入等跨版本通用中间件,直接注册在根路由r.Use(...)上。
第二步:若v2需要签名验证而v1不需要,则在创建v2分组时传入中间件:v2 := r.Group("/v2", common.VerifySign)。这比在v2内部每个路由都写v2.Use(...)更安全,避免漏配。
第三步:切勿只给v1加鉴权中间件而忘了v2——【v1裸奔而v2被保护是线上高频事故】,所有安全相关中间件必须显式覆盖每个需要保护的分组。
响应数据构造必须版本隔离
Handler函数只负责业务流程和错误处理,原始领域数据(如userDomain := svc.GetUser(id))获取后,立即交给版本专用转换函数:c.JSON(200, toResponseV1(userDomain))或c.JSON(200, toResponseV2(userDomain))。
toResponseV1和toResponseV2必须各自定义输出结构体,字段增减、类型变更、嵌套层级变化全部在转换层完成,严禁靠json:"name,omitempty"动态控制——它无法表达“v1绝对不返回该字段”的契约约束。
数据库变更必须对齐版本:v2新增NOT NULL字段时,v1接口不能因此返回500,service层要兜底填充默认值或降级返回精简数据。
v1下线与流量迁移
当v1正式停用,不要直接删除路由,而是注册一个捕获所有/v1/*的兜底路由:r.Any("/v1/*path", handleV1Deprecated)。
handleV1Deprecated中统一返回HTTP 410 Gone,并记录请求来源、User-Agent、调用量,用于监控残留客户端。
若需引导迁移,可返回301重定向到对应v2路径,但仅限GET接口;POST/PUT等非幂等请求严禁301,避免重复提交。



















