Gin 中统一捕获 panic 需用 gin.RecoveryWithWriter() 替代默认 gin.Recovery(),在恢复后调用 c.AbortWithStatusJSON() 写响应;业务 error 须显式调用 c.Error() 并配合 c.Abort(),ErrorHandler 中间件须注册在业务中间件之后、gin.Recovery() 之前。

Go Gin 中间件里怎么统一捕获 panic 并转成 HTTP 错误
直接说结论:Gin 本身不提供“全局错误处理函数”这种抽象,但可以通过 recovery 中间件 + 自定义错误包装机制实现等效效果。关键不是注册一个函数,而是控制 panic 恢复后怎么写响应、是否记录、要不要透出堆栈。
常见错误是只加了 gin.Recovery(),但没替换默认行为——它默认只打日志、返回 500,且不包含错误详情,前端拿不到 message 字段。
- 必须用
gin.RecoveryWithWriter()替代默认gin.Recovery(),才能接管响应逻辑 - 恢复 panic 后,要主动调用
c.AbortWithStatusJSON()或c.JSON(),否则响应可能已写出一半 - 别在 recover 函数里直接 panic,会导致二次崩溃;所有错误处理必须同步完成
如何让自定义 ErrorHandler 接收业务层抛出的 error(非 panic)
Gin 的中间件链是线性的,error 不会自动向上冒泡。所谓“全局处理”,其实是靠每个 handler 主动调用 c.Error() 把 error 注入上下文,再由后续中间件读取 c.Errors。
典型场景:数据库查询失败、参数校验不通过、第三方服务超时——这些都该用 return + c.Error() 显式上报,而不是 panic。
立即学习“go语言免费学习笔记(深入)”;
- 业务 handler 中:遇到 error 就写
c.Error(err),然后return,不要继续执行 - 错误收集中间件中:遍历
c.Errors,取第一个非 nil 的Err,按类型匹配(比如判断是不是*app.ValidationError) - 注意
c.Error()不会中断流程,必须配合c.Abort()或提前 return,否则可能多次写响应体
为什么不能只依赖 defer+recover 写自己的中间件
自己写 defer func() { if r := recover(); r != nil { ... } }() 看似灵活,但容易踩三个坑:
- Gin 的
recovery中间件已注册在默认栈顶,你的自定义 recover 可能根本捕不到 panic(顺序问题) - 没处理好
http.ResponseWriter状态码和 header 写入时机,可能触发http: multiple response.WriteHeader calls - 忽略
gin.Context的生命周期,recover 后继续用c可能引发 panic(比如 c.Request.Body 已关闭)
正确做法是直接用 gin.RecoveryWithWriter(),传入自定义 writer 和错误处理函数,把控制权交给 Gin 的标准流程。
Gin ErrorHandler 怎么区分开发/生产环境输出细节
核心是判断 gin.Mode(),但别硬编码 "release" 字符串——用 gin.IsProduction() 更可靠。
开发环境可以返回完整 error message + stack trace(用 debug.Stack()),生产环境必须脱敏,只返回用户友好的提示,比如 "系统繁忙,请稍后再试"。
- stack trace 必须从
recover()的值里提取,不能依赖err.Error()(它通常不含堆栈) - 如果用了第三方错误包(如
pkg/errors或github.com/ztrue/tracerr),注意它们的.StackTrace()方法是否在生产模式下被禁用 - HTTP 状态码别全写 500:参数错误用 400,未授权用 401,资源不存在用 404,服务不可用才用 503
最易被忽略的一点:ErrorHandler 中间件的注册顺序。它必须放在所有业务中间件之后、但要在 gin.Recovery() 之前——否则 panic 被默认 recovery 捕获了,你的逻辑就失效了。实际部署时,常有人把自定义 error 处理器放在 router.Use() 最前面,结果什么也抓不到。










