必须替换默认 recovery 中间件,因其返回 html、无 code 字段且不处理业务错误;需用 customrecovery() 替代并前置注册,配合 errorhandler() 统一 json 响应,错误码与 http 状态码解耦,集中定义、严格结构、禁止硬编码。
默认的 gin.recovery() 不能用于微服务错误统一,它返回 html、无 code 字段、不处理业务错误——直接用会卡死联调,前端解析失败。
为什么必须替换默认 Recovery 中间件
默认 gin.Recovery() 在 panic 时返回的是带堆栈的 HTML 页面(text/html),不是 JSON;且响应体里没有 code 字段。一旦某个服务漏掉替换,网关或前端收到 text/html 就会解析报错,整个链路中断。
更关键的是:它只捕获 panic,对 return errors.New("xxx")、参数校验失败等“非崩溃型错误”完全无感——这些错误若没被后续中间件检查 c.Errors,就直接以 200 状态码 + 空/脏响应体返回,前端根本收不到错误信号。
- 必须用自定义
CustomRecovery()替换,并注册在所有中间件最前面:router.Use(CustomRecovery()) -
CustomRecovery()里c.AbortWithStatusJSON()后必须加return,否则可能触发header already written - panic 的
err必须传给结构化日志(如logger.Errorw("PANIC", "err", err)),不能只fmt.Printf
如何让业务错误(如参数校验)也走统一 JSON 格式
Gin 的 c.Error() 只是往 c.Errors 队列里塞错误,不自动渲染响应。你得在请求生命周期末尾主动检查并转换——靠中间件兜底,而不是每个 handler 里手写 c.JSON()。
典型做法是加一个 ErrorHandler() 中间件,放在 CustomRecovery() 之后、路由执行完再触发:
- 调用
c.Next()让路由链继续,结束后检查len(c.Errors) > 0 - 用
errors.Is(err.Err, ErrInvalidParam)判断错误类型,别用err.Error() == "xxx"字符串匹配 - 每个业务错误(如
ErrInvalidParam)应实现Unwrap(),支持链式错误追踪 - 确保只在一个地方调用
c.AbortWithStatusJSON(),重复调用会 panic
错误码与响应结构设计的关键约束
HTTP 状态码和业务 code 必须解耦:前者给网关/Nginx 看,后者给前端解析。比如参数错误该返回 HTTP 400,但业务 code 是 1001,不是 400。
- 错误码集中定义在
pkg/errcode/errcode.go,用const+ 注释,例如ErrUserNotFound = 4001 - 响应结构体字段严格为
code/msg/data,禁用status、message、errorCode等不一致命名 - 禁止在 handler 里硬编码
c.JSON(400, gin.H{"code": 1001}),必须通过工厂函数如errcode.NewBadRequest(errcode.UserNotFound) - 生产环境响应中禁用
c.Errors.Last().Error()直接输出,避免暴露数据库路径、SQL 等敏感信息
容易被忽略的细节:c.AbortWithError() 和 c.Error() 的区别
c.AbortWithError() 不会返回任何响应,它只是把错误塞进 c.Errors 并中断中间件链——如果你没配 ErrorHandler(),这个错误就丢了,客户端收到的仍是 200 + 空体。
c.Error() 同理,也是信号机制,不是输出动作。两者都依赖下游中间件读取 c.Errors 并真正写响应。
- 不要在
c.AbortWithError()后面紧跟c.JSON(),会触发http: multiple response.WriteHeader calls - 检查错误是否存在的正确方式是
len(c.Errors) > 0,不是err != nil - 如果错误实现了
Status() int接口(如自定义AppError),可从中提取 HTTP 状态码 fallback 使用











