默认gin.Recovery()仅捕获panic,不处理return errors.New等非崩溃错误;c.Error()仅存入错误队列而不响应;CustomRecovery需前置注册并禁用默认Recovery;ErrorHandler中间件须取c.Errors.Last()映射为含code/message/status/request_id的JSON响应。

默认 gin.Recovery() 不能拦截内部微服务错误,它只捕获 panic,对 return errors.New("xxx")、校验失败、下游 HTTP 5xx 等“非崩溃型错误”完全无感——这些错误若没被显式处理,就会漏过统一格式,前端收到 200 + 非结构化 body,联调直接卡住。
为什么 c.Error() 不会自动触发 JSON 响应
c.Error() 只是把错误推入 c.Errors 队列,不中断流程、不写响应、不带 code 字段。它本质是中间件间传递信号的“邮筒”,不是“出口”。你必须在请求生命周期末尾主动检查并渲染。
- 常见错误:在 handler 里调用
c.Error(ErrInvalidParam)就以为完事了,结果返回 200 空响应 - 正确做法:所有业务错误都该走
c.Error(),但最终响应必须由统一中间件兜底输出 - 检查条件必须是
len(c.Errors) > 0,不能靠err != nil判断——因为c.Error()不返回 err
CustomRecovery 必须前置注册且禁用默认 Recovery
默认 gin.Recovery() 和自定义错误中间件共存时,容易触发 http: multiple response.WriteHeader calls panic——两者都试图写响应头。必须先 router.Use(CustomRecovery),再禁用默认 recovery(不调 router.Use(gin.Recovery()))。
-
CustomRecovery的defer里必须有return,否则c.Next()继续执行,可能二次写响应 - 它里面不该调
c.Error(),而应直接c.AbortWithStatusJSON(500, ...)输出结构化 JSON - panic 日志必须用
logger.Errorw("PANIC", "err", err, "stack", debug.Stack()),不能只fmt.Printf,否则丢堆栈
ErrorHandler 中间件如何映射业务错误到 JSON
这是真正把 c.Errors 转成标准响应的地方,必须放在 CustomRecovery 之后、路由之前(router.Use(ErrorHandler()))。
立即学习“go语言免费学习笔记(深入)”;
- 取
c.Errors.Last()而非First(),因中间件顺序执行,最后塞入的通常是主业务错误 - 用
errors.Is(err.Err, ErrInvalidParam)做类型匹配,别用err.Error() == "参数错误"字符串比对 - 响应结构必须含
code(如 1001)、message、status(HTTP 状态码)、request_id四字段,例如:{"code":1001,"message":"参数错误","status":400,"request_id":"abc123"} - 避免在多个中间件里重复调
c.AbortWithStatusJSON,否则 panic;确保只有一个出口
AppError 类型和错误码常量怎么定义才不踩坑
硬编码 c.JSON(400, gin.H{"code": 1001}) 是最常见退化点——错误码散落、无法导出、语义模糊、升级难维护。
- 错误码必须集中定义在
pkg/errcode/errcode.go,用const+ 注释,如ErrUserNotFound = 4001 // 用户不存在 -
AppError结构体要实现Status() int方法,方便中间件提取 HTTP 状态码 - 工厂函数如
errcode.NewBadRequest(errcode.UserNotFound)返回 *AppError,禁止裸errors.New - 敏感信息(SQL 片段、文件路径)不能进
message字段,AppError应只含可暴露内容
真正难的不是写中间件,而是让 panic、业务 error、HTTP status、trace ID、日志字段全部对齐——少一个,链路就断一截。比如 request_id 没透传到错误响应里,运维查问题就得翻三份日志拼时间戳。


















