gin默认recovery中间件破坏微服务错误一致性,因其返回html而非json且无code字段;需用customrecovery替换并前置注册,配合errorhandler中间件统一处理c.errors,响应结构code用int类型,网关层兜底收敛非标错误。

为什么 Gin 默认 recovery 中间件会破坏微服务错误一致性
因为 gin.Recovery() 默认返回 HTML 错误页(含堆栈),不是 JSON,且不带业务 code 字段。一旦某个服务忘了替换它,前端收到 text/html 响应就会解析失败,联调直接卡住。
更隐蔽的问题是:它只捕获 panic,对主动 return errors.New("xxx") 或校验失败这类“非崩溃型错误”完全无感——这些错误若没被中间件显式检查,就会漏过统一格式处理。
- 必须用自定义
CustomRecovery替换默认gin.Recovery(),且注册顺序要放在最前(router.Use(CustomRecovery)) - 不要在 handler 里手动
c.AbortWithStatusJSON(500, ...)处理 panic,否则和 recovery 冲突,可能触发header already written - 如果用了
zap日志,recover 捕获的err必须传给logger.Errorw("PANIC", "err", err),不能只fmt.Printf,否则丢堆栈
如何让业务错误(如参数校验失败)也走统一 JSON 格式
Gin 的 c.Error() 是个信号机制,不是响应动作。它把错误塞进 c.Errors 队列,但不会自动渲染——你得在请求生命周期末尾主动检查并转换。
典型做法是在 recovery 中间件之后、路由执行完再加一层“错误兜底中间件”,检查 c.Errors 是否非空:
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
// 取第一个 error,按类型映射业务 code
err := c.Errors.Last()
switch {
case errors.Is(err.Err, ErrInvalidParam):
c.AbortWithStatusJSON(http.StatusBadRequest, response.Error(1001, "参数错误"))
case errors.Is(err.Err, ErrUserNotFound):
c.AbortWithStatusJSON(http.StatusNotFound, response.Error(4001, "用户不存在"))
default:
c.AbortWithStatusJSON(http.StatusInternalServerError, response.Error(5000, "系统异常"))
}
return
}
}
}
-
c.Error()要配合errors.Is()或自定义 error 类型判断,别用err.Error() == "xxx"字符串匹配 - 避免在多个中间件里重复调用
c.AbortWithStatusJSON,否则 panic;确保只在一处出口响应 - 业务 error 工厂函数(如
ErrInvalidParam)应带Unwrap()方法,支持链式错误追踪
统一响应结构体里 code 字段该用 int 还是 string
用 int。前端 JavaScript 用 switch(code) 比 switch(code.toString()) 更安全,也避免 JSON 解析时类型歧义(比如 "1001" 和 1001 在某些弱类型客户端表现不一致)。
约定:0 表示成功;1000–1999 为参数/客户端类错误(如 1001 参数缺失);4000–4999 为资源类错误(如 4001 用户不存在);5000+ 为服务端系统错误。
- 别把 HTTP 状态码直接当
code值用(比如把404当作业务码),否则语义混淆——HTTP 状态码控制网络层行为,code控制前端业务分支 - 如果微服务需对接外部系统(如 Java Spring Cloud),可额外加
errorCode字段存字符串码,但主code仍保持 int -
message字段必须是 string 类型,不能是*string,空提示就填"",避免前端解包 panic
网关层如何兜底收敛下游服务的异常格式不一致
Spring Cloud Gateway 或 Kong 这类网关,不能假设所有下游服务都已正确实现统一错误。必须配置 fallback 逻辑:当响应 Content-Type 不是 application/json,或 JSON body 缺少 code 字段时,强制重写为标准格式。
例如在 Spring Cloud Gateway 的 GlobalFilter 中:
if (response.getStatusCode().is5xxServerError()) {
// 检查 body 是否为标准 JSON
if (!hasStandardErrorFields(body)) {
rewrittenBody = Map.of("code", 5000, "message", "服务暂时不可用", "timestamp", System.currentTimeMillis());
exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
- 网关必须透传
X-Request-ID到下游,并在错误响应中回填到trace_id字段,否则前后端联调无法串起日志 - 不要在网关做复杂业务码映射(如把 Java 服务的
"USER_NOT_FOUND"转成 Go 服务的4001),那属于服务内部职责;网关只做“非标→标”的清洗 - 如果下游返回了原始数据库错误(如
"pq: duplicate key violates unique constraint"),网关应拦截并降级为通用提示,而非透传
真正难的不是写一个 CustomRecovery,而是让所有团队成员在每个 if err != nil 分支里,都调用同一个 response.Error() 而不是手写 c.JSON(400, gin.H{...})。规范落地靠约束,不靠自觉。











