必须保留旧路由入口并做内部桥接,而非直接改前缀或删路由;通过gin.New()初始化空白引擎,在旧handler中用http.Client透传header、query、body调用新服务,并原样返回响应。

微服务拆分后,老接口调用直接 404 或 panic 怎么办
直接改路由前缀或重写 handler 不行——用户端、第三方系统、前端 SDK 都在硬编码旧路径。Gin 的 router.Group 本身不提供“路径别名”或“自动转发”,强行删旧路由会立刻中断调用。
真正能落地的兼容方案是:**保留旧路由入口,内部做逻辑桥接**。不是靠 Nginx 转发(那绕过了 Gin 中间件链),而是让同一个 GET("/v1/users") 请求,在不改客户端的前提下,实际由新拆分出的 user-service 模块处理。
- 用
gin.New()初始化空白引擎,避免gin.Default()自带中间件干扰桥接逻辑 - 在旧路由 handler 里用
http.Client同步调用新服务(注意超时和错误 fallback) - 关键:把原
Context的 header(如x-token)、query、body 全部透传,否则鉴权/参数丢失 - 响应状态码、header(如
Content-Type)、body 原样返回,客户端无感
如何让 /api/v1 和 /svc/user/v1 共存且共享中间件
模块化拆分后,你希望 /api/v1/users(兼容层)和 /svc/user/v1/users(新服务真实入口)都走同一套 JWT 鉴权、日志、限流,但 Group.Use() 只作用于本组内路由。
正确做法是:**把中间件注册到根路由,再用条件判断跳过特定路径**。比如:
r := gin.New()
r.Use(JWTAuth(), Logger(), RateLimit())
r.GET("/api/v1/*path", ProxyToUserService) // 兼容入口
r.GET("/svc/user/v1/*path", UserHandler) // 新入口
在 JWTAuth() 中加一层判断:
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
if strings.HasPrefix(c.Request.URL.Path, "/api/v1/") {
// 兼容层:跳过鉴权(由 proxy 侧统一做),或只校验必要 header
c.Next()
return
}
// 正常鉴权流程
token := c.GetHeader("x-token")
// ...
}
}
这样既复用中间件代码,又避免对代理路径重复鉴权导致失败。
拆分后跨服务调用的 Context 透传失效
你在 /api/v1/orders 里调用 user-service 的 /svc/user/v1/profile,但对方收不到 trace-id、request-id 或自定义 header(如 x-user-id)。这是因为 http.Client 默认不透传所有 header,且 Gin 的 c.Request.Header 是只读映射。
- 手动复制关键 header:
req.Header.Set("x-trace-id", c.GetHeader("x-trace-id")) - 不要用
c.Request.Header.Clone()—— 它不会复制 Go 内部的 canonical key 映射,可能导致 header 丢失 - 更稳妥的方式:用
c.Request.Header.Clone()+ 显式遍历c.Request.Header键值对,过滤掉Connection、Host等禁止转发的 header - 如果用 gRPC-Web,优先走
metadata.MD透传,比 HTTP header 更可靠
版本升级时 /v1 → /v2 接口字段结构不兼容
不是简单加个 router.Group("/v2") 就完事。比如 v1.User 返回 {"name":"a"},而 v2.User 改成 {"full_name":"a"},前端直接炸。
Gin 本身不提供字段级兼容能力,必须自己封装转换层:
- 定义 v1 → v2 的 mapping 函数:
func V1UserToV2(u V1User) V2User { ... } - 在
/v2/usershandler 里先调 v1 接口(或 DB 查询),再做字段映射 - 重点:错误码也要对齐。v1 返回
404,v2 不能改成400;用c.Status(404)强制设状态码 - 别依赖 JSON tag 自动转换(如
json:"full_name,omitempty")——它解决不了字段语义变更
最易被忽略的是时间格式:v1 用 "2006-01-02",v2 改成 RFC3339,前端解析会静默失败。所有日期字段必须显式格式化,不能靠 time.Time 默认 MarshalJSON。


















