recovery中间件通过go原生defer+recover机制捕获panic,必须在同goroutine中且panic前注册;默认写堆栈到os.stderr并返回500空响应;定制需用customrecovery传入recoveryfunc;位置须在所有可能panic的中间件和handler之前。

Recovery中间件为什么能捕获 panic
它靠的是 Go 原生的 defer + recover 机制,不是魔法。Gin 在每个请求进入时,会在 handler 执行前插入一个 defer 匿名函数,里面调用 recover() 捕获当前 goroutine 的 panic。
关键点在于:这个 defer 必须在 panic 发生前已注册,且必须在同一个 goroutine 内 —— Gin 的 handleHTTPRequest 流程保证了这一点。
-
Recovery()返回的HandlerFunc本质就是个包裹了defer recover的闭包 - panic 后不会终止整个服务,只中断当前请求的 handler 链,
c.Next()不再执行,但defer会触发 - 如果 panic 发生在
goroutine里(比如启了个新协程),Recovery 中间件完全捕获不到 —— 这是常见盲区
Recovery 默认行为:日志输出到哪里、返回什么状态码
默认使用 Recovery() 时,错误堆栈会写入 os.Stderr(即终端 stderr),同时返回 HTTP 500 状态码和空响应体。
这不是硬编码,而是由 DefaultErrorWriter 和 defaultHandleRecovery 共同决定的:
-
DefaultErrorWriter是io.Writer类型,默认指向os.Stderr -
defaultHandleRecovery函数负责写响应:c.AbortWithStatus(http.StatusInternalServerError) - 它不写任何响应 body,也不设置
Content-Type,所以客户端收到的是纯 500 + 空 body
CustomRecovery 怎么定制 panic 处理逻辑
想记录到文件、上报 Sentry、或返回结构化错误?必须用 CustomRecovery 或 CustomRecoveryWithWriter。
核心是传入一个符合 RecoveryFunc 类型的函数:
func(c *gin.Context, err any) {
// err 就是 recover() 拿到的值,可能是 string、error、甚至自定义 struct
log.Printf("panic captured: %+v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "Internal Server Error",
"trace": debug.Stack(),
})
}
- 注意:这个函数里不能再 panic,否则会二次崩溃
- 调用
c.Abort()是安全的,但不要重复调用c.JSON()或c.Status()—— Recovery 已经接管了响应流程 - 如果写了响应又没 abort,可能和后续中间件冲突,导致 “write header after body” 错误
Recovery 中间件在中间件链里的位置很重要
它必须放在所有可能 panic 的中间件和路由 handler 之前,否则捕获不到。
典型错误写法:
r := gin.New() r.Use(MyPanicProneMiddleware()) // ❌ 这个中间件里 panic,Recovery 还没注册,就崩了 r.Use(gin.Recovery())
正确顺序:
- 全局注册:先
r.Use(gin.Recovery()),再r.Use(...)其他中间件 - 路由组注册:在
r.Group(...)调用前就UseRecovery,否则组内 handler 不受保护 -
gin.Default()自动做了这事 —— 它内部调用顺序是engine.Use(Logger(), Recovery())
真正容易被忽略的是:Recovery 只对它之后注册的 handler 生效,而中间件执行顺序严格按 Use 调用先后。写错顺序,等于没加。











