URL路径版本号必须置于第一级路径,如/v1/users或/api/v1/users(仅当/api下存在非API路由时),以保持REST资源语义;/v1和/v2须为独立RouterGroup,结构体、handler、中间件严格分离,废弃版本返回410 Gone并带Sunset头。

URL路径版本号必须放在第一级路径
不是/api/v1/users,也不是/users/v1或/v1/api/users,正确写法只有/v1/users或/api/v1/users(前提是/api下还存在非API路由,比如/api/health)。否则就破坏了REST资源语义——/users是资源,版本是访问维度,不该嵌套进资源路径里。
常见错误现象:/v1/users?version=v2看似灵活,但查询参数不参与路由匹配,中间件无法识别版本,CDN缓存会把不同版本响应混在一起,导致客户端拿到错版数据。
-
/v1和/v2必须是独立的RouterGroup,不能共用同一组再靠逻辑分支判断 - 如果项目已有
/health、/metrics等非业务端点,才考虑加/api前缀;否则直接/v1更干净 - 路径中禁止出现多个版本段,如
/v1/admin/v2/users——这会让路由不可维护且语义混乱
用Group()隔离版本逻辑,别在handler里if-else
把getUsersV1和getUsersV2写成两个独立函数,分别挂到v1.GET("/users", getUsersV1)和v2.GET("/users", getUsersV2),而不是在一个getUsers里用if version == "v1"硬编码分支。前者支持独立演化、单独测试、按需加载中间件;后者一旦逻辑耦合,改v2就可能误伤v1。
性能影响明显:Gin的路由树在启动时静态构建,Group()生成的是不同前缀的子树,匹配O(1);而运行时判断版本号属于额外CPU开销,还绕过Gin的路由优化。
立即学习“go语言免费学习笔记(深入)”;
- 每个版本组可绑定专属中间件,比如
v2加validateV2Request,v1保持旧校验逻辑 - 结构体定义严格分离:
UserV1和UserV2各自声明,避免用omitempty临时打补丁 - 不要复用handler函数名,哪怕逻辑相似——命名即契约,
createUserV2意味着它只对v2负责
废弃旧版本时必须返回410 Gone而非404 Not Found
404会让客户端误以为路径写错了,反复重试;410 Gone明确表示“这个版本曾经存在,现在永久移除”,配合Deprecation响应头(如Deprecation: true; Sunset: Wed, 01 Jan 2026 00:00:00 GMT),能驱动客户端主动升级。
容易踩的坑是:下线/v1后直接删掉路由组,结果请求进来被根路由兜底返回404。正确做法是在v1组里显式注册兜底handler:
func deprecatedV1Handler(c *gin.Context) {
c.Header("Deprecation", "true")
c.Header("Sunset", "Wed, 01 Jan 2026 00:00:00 GMT")
c.AbortWithStatusJSON(410, gin.H{"error": "v1 API is no longer available"})
}
// 然后在v1组末尾挂载
v1.Any("/*path", deprecatedV1Handler)
- 别依赖文档或公告通知下线——HTTP状态码才是机器可读的信号
-
Sunset头必须是GMT格式,且时间点应早于实际下线时间,留出缓冲期 - 监控调用量,当
v1日均请求低于阈值(比如0.1%)再执行最终清理
模块化路由文件里,版本分组要跨文件复用
如果你按模块拆了user.go和order.go,每个文件里都得支持多版本,就不能让v1和v2逻辑散落在各处。正确方式是:在router.go总入口里创建v1和v2组,再把各模块的注册函数传进去:
// router/user.go
func RegisterUserRoutes(g *gin.RouterGroup, version string) {
if version == "v1" {
g.GET("/users", getUsersV1)
g.POST("/users", createUserV1)
} else if version == "v2" {
g.GET("/users", getUsersV2)
g.POST("/users", createUserV2)
}
}
// router.go
v1 := r.Group("/v1")
RegisterUserRoutes(v1, "v1")
RegisterOrderRoutes(v1, "v1")
v2 := r.Group("/v2")
RegisterUserRoutes(v2, "v2")
RegisterOrderRoutes(v2, "v2")
这样既保持模块边界清晰,又避免重复写Group()和条件判断。最常被忽略的是:不同模块的v2接口可能依赖同一套新中间件,而这种结构能让你在v2组上统一.Use(),不用每个模块自己重复加。



















