应优先使用 gin.headerselector 基于 accept 头实现版本路由分流,若用 x-api-version 则须在最外层中间件解析并设 c.set("api_version", v),配合 /v1、/v2 路由分组实现权限、监控等差异化控制。

用 gin.HeaderSelector 提前读取版本头,别等进 handler 再判断
Header 分流必须在请求刚进来、路由匹配完成但 handler 还没执行时就完成解析——gin.HeaderSelector 就是干这事的。它不是中间件,而是 Gin 内置的「路由选择器」,允许你基于请求头动态决定走哪条路由分支。但注意:它只支持 Accept 头(比如 Accept: application/vnd.myapi.v2+json),不支持自定义头如 X-API-Version。
如果你坚持用 X-API-Version,就得自己写中间件,在 c.Request.Header.Get("X-API-Version") 之后立刻 c.Set("version", v),并确保这个中间件挂载在所有 Group 的最外层(比路由注册更早执行)。常见错误是把它放在某个子 Group 里,结果 /v1/users 和 /v2/users 共享同一个中间件实例,却没做路径隔离,导致版本识别错乱。
r.Group("/v1") 和 r.Group("/v2") 是必须的,不是可选的
路径前缀分组不是为了“看着整齐”,而是 Gin 路由树结构、中间件作用域、OpenAPI 工具链识别的基础。没有 v1 := r.Group("/v1"),你就没法给 v1 单独挂 v1AuthMiddleware,也没法让 Prometheus metrics 自动打上 version="v1" 标签。
- 硬编码
r.GET("/users?v=1")或r.GET("/users/v1"):工具链无法识别版本语义,Nginxlocation /v1/规则失效 - 把
/v1和/v2都挂在根r.Use()下:权限逻辑混在一起,v2 要求 admin、v1 只要 user,根本没法差异化控制 - 在 Group 外又写了
r.GET("/v1/:id", ...):会贪婪匹配/v1/users,导致v1.Group("/users")下的所有路由 404
中间件里别硬编码版本判断,用 c.FullPath() 动态提取
写一个通用鉴权中间件,而不是为每个版本写一个函数。关键不是“这是 v1 吗”,而是“这个请求路径属于哪个版本”——c.FullPath() 比 c.Request.URL.Path 更可靠,它已处理了路由参数(比如 /v1/users/:id 匹配到 /v1/users/123 时仍返回 /v1/users/:id)。
示例逻辑:
func VersionedAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
path := c.FullPath()
var version string
if strings.HasPrefix(path, "/v1/") {
version = "v1"
} else if strings.HasPrefix(path, "/v2/") {
version = "v2"
} else {
c.AbortWithStatusJSON(400, gin.H{"error": "unknown version"})
return
}
role := c.GetString("role") // 前置 auth 中间件注入
if !allowed(role, version, path) { // 查策略表或 map
c.AbortWithStatusJSON(403, gin.H{"error": "forbidden"})
return
}
c.Next()
}
}
注意:别用 c.JSON(403, ...) + c.Abort() 两步,漏掉 Abort() 就会让 handler 继续执行。
不要在版本校验中间件里 c.Abort(),把错误响应控制权留给统一错误处理
很多中间件一发现版本不合法就直接 c.AbortWithStatusJSON(400, ...),这会导致后续的全局 Recovery 或自定义错误中间件完全失效。Gin 的错误处理链依赖 c.Next() 流程完整走完,才能触发 recovery 或你写的 GlobalErrorMiddleware。
正确做法是:c.Set("version_error", "v3 not supported"),然后 c.Next() 让请求继续往下走;在最后的 handler 或全局 error middleware 里检查这个 key,再统一格式化响应。这样既能保持错误语义,又不破坏中间件洋葱模型。
最容易被忽略的一点:c.Set() 的 key 必须全局唯一。别两个中间件都用 "version",后设的会覆盖前设的——建议用带命名空间的 key,比如 "api_version" 或 "myapp.version"。











