gin 的 gin.recovery() 仅记录日志并返回固定500响应,不透出panic值、不执行自定义错误映射、不调用c.error(),且会拦截后续recover逻辑;需禁用默认中间件并手写带堆栈记录与统一响应的panicrecovery中间件,同时覆盖业务error和goroutine panic。

为什么 Gin 的 gin.Recovery() 不能满足你的错误处理需求
Gin 默认启用的 gin.Recovery() 中间件确实会 recover panic,但它只做两件事:记录日志 + 返回 500 Internal Server Error 响应体({"error":"Internal Server Error"}),不透出 panic 值、不执行你定义的错误映射逻辑、也不调用 c.Error() 或写入自定义响应结构。它就像个“静默守门人”,抢在你之前把 panic 吞掉,导致你在自己的中间件里写 defer func(){ if r := recover(); r != nil { ... } }() 完全无效。
- 开发时务必关掉它:
gin.SetMode(gin.DebugMode),否则连原始 panic 堆栈都看不到,调试像盲人摸象 - 生产环境必须自己重写 recovery 中间件,且要放在
r.Use(...)链最前面,否则后续中间件(比如日志)可能已写 header,再 abort 就会 panic -
recover()只返回interface{},不是error;想获取堆栈得用debug.PrintStack()或runtime/debug.Stack()
怎么写一个真正可用的 panic 捕获中间件
核心是三件事:recover → 记录(含堆栈)→ 统一响应 → 中断流程。别只打印 panic 值,那对定位线上问题几乎没用。
- 必须调用
c.AbortWithStatusJSON(500, ...)或手动c.Abort()+c.JSON(),否则c.Next()后续 handler 还会执行,可能重复写 body 或 panic - 堆栈信息建议存到日志字段(如
stack=string(debug.Stack())),而非直接返回给前端——安全和最小信息披露原则 - 示例中间件:
func PanicRecovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
stack := debug.Stack()
log.Printf("PANIC in %s %s: %v\n%s", c.Request.Method, c.Request.URL.Path, r, stack)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "server error",
"data": nil,
})
}
}()
c.Next()
}
}
使用时确保它是第一个:r.Use(PanicRecovery(), gin.Logger(), AuthMiddleware())
如何统一处理 handler 显式返回的 error
Gin 本身不自动处理 error 返回值,必须靠 c.Error(err) 主动注入,再由中间件检查 c.Errors 列表。这是和 panic 处理完全正交的路径,两者都要覆盖才算完整。
- 所有业务 handler 内部,遇到错误优先调用
c.Error(err),而不是直接c.JSON(400, ...)—— 这样才能被统一中间件捕获 - 中间件里用
c.Errors.Last().Err取出 error,再通过errors.As()或类型断言匹配自定义错误(如*AppError),提取Code和Message - 别用
fmt.Errorf直接拼错,定义带码结构体:type AppError struct { Code int; Msg string; Err error },并实现ErrorCode() int方法,方便中间件反射提取
容易被忽略的 goroutine 错误兜底
HTTP 请求生命周期外的 panic(比如定时任务、MQ 消费 goroutine)完全不会经过 Gin 中间件。这部分必须单独包装,否则服务会静默崩溃或丢消息。
- 每个独立 goroutine 启动时都加一层 defer:
go func() { defer func(){ if r:=recover(); r!=nil { log.Println("goroutine panic:", r) } }(); doWork() }() - 更稳妥的做法是封装启动函数:
GoSafe(func(){...}),内部统一 recover + 上报(如发 Sentry 或写日志) - 注意:不要在 defer 里调用
c.Abort()或任何依赖*gin.Context的操作——goroutine 里根本没有上下文
真正难的不是写一个 recovery 函数,而是让 panic、业务 error、goroutine error 全部进入同一套错误分类、日志字段、告警通道。漏掉任意一环,线上问题就少一条线索。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











