URL路径嵌入版本号(如/v1/users)是Go微服务中最优选择,因其支持CDN缓存、Nginx路由、OpenAPI生成及前端感知;Header方案需手动解析且易出错;query参数不可缓存且不语义。
API 版本号该放在 URL 还是 Header?
Go 微服务中,URL 路径嵌入版本号(如 /v1/users)是最常见且最易调试的选择。它天然支持 CDN 缓存、Nginx 路由分发、OpenAPI 文档生成,也方便前端直接感知变更。而用 Accept 或自定义 Header(如 X-API-Version: v2)虽更“RESTful”,但在 Go 的 HTTP 路由层(gorilla/mux、chi、net/http.ServeMux)中需手动解析、路由分发,容易漏判或覆盖错误。
实际项目中,URL 路径版本的可维护性远高于 Header 方案——尤其当多个团队并行开发 v1/v2 接口时,路径隔离能避免 handler 混杂和中间件错配。
- 别用
query 参数(如?version=v2),它不可缓存、不语义、不支持路由树匹配 - 如果必须用 Header(例如遗留系统强约束),请在中间件中统一提取并注入到
context.Context,后续 handler 通过ctx.Value()获取,而非重复解析请求头 - 版本前缀统一用小写
v1、v2,避免api/v1和v1/api混用导致路由歧义
用 chi 或 gorilla/mux 实现多版本路由隔离
以 chi 为例,它支持嵌套路由器,天然适合按版本切分:
func main() {
r := chi.NewRouter()
// 公共中间件(日志、recover、CORS)
r.Use(middleware.Logger, middleware.Recoverer)
// v1 路由组
v1 := chi.NewRouter()
v1.Get("/users", v1GetUsersHandler)
v1.Post("/users", v1CreateUserHandler)
r.Mount("/v1", v1)
// v2 路由组(字段校验、响应结构、DB 查询逻辑可能不同)
v2 := chi.NewRouter()
v2.Get("/users", v2GetUsersHandler) // 返回带 avatar_url 字段
v2.Post("/users", v2CreateUserHandler) // 新增 nickname 校验
r.Mount("/v2", v2)
http.ListenAndServe(":8080", r)
}
关键点:
- 每个版本用独立
chi.Router,避免 handler 互相污染 - 不要在同一个路由组里写
r.Get("/v1/users", ...)和r.Get("/v2/users", ...)—— 这会丢失路由树结构,也不利于中间件按版本启用(比如 v2 需要额外鉴权) - 若用
gorilla/mux,等价做法是:先subr := r.PathPrefix("/v1").Subrouter(),再注册子路由
如何复用结构体又保持 v1/v2 响应兼容?
硬编码两个完全独立的 struct(UserV1、UserV2)会导致重复逻辑和序列化冗余。更实用的做法是:一个基础 struct + tag 控制序列化行为 + 构造函数封装差异。