Recovery中间件通过defer+recover组合捕获panic,必须置于中间件链最前端且仅捕获当前goroutine内c.Next()调用栈中的panic;跨协程、init/main中或请求生命周期外的panic无法被捕获。

Recovery 中间件怎么捕获 panic
它靠的是 defer + recover() 组合,不是监听或钩子。中间件在请求开始时注册一个 defer 匿名函数,等整个请求链(包括所有后续中间件和 handler)执行完或中途 panic 时触发。只要 panic 发生在 c.Next() 调用栈内,就能被截住。
关键点在于:这个 defer 必须在最外层 handler 执行前就注册好——Gin 把 Recovery 放在中间件链最前面(engine.Use(Logger(), Recovery())),所以它总能兜底。
- 如果 Recovery 不在链首,比如写成
r.Use(A(), Recovery(), B()),那 A() 里 panic 就捕获不到 -
recover()只能捕获当前 goroutine 的 panic,跨 goroutine 的(如启了新协程后 panic)不会被捕获 - recover 后必须显式终止后续流程,否则
c.Next()还会继续往下走——Gin 的 Recovery 内部调用了c.Abort()
RecoveryWithWriter 和 CustomRecovery 的区别在哪
核心差异是错误处理逻辑是否可定制:Recovery() 和 CustomRecovery() 都是封装好的快捷入口,最终都调用 RecoveryWithWriter();而 RecoveryWithWriter() 允许你指定日志输出目标(io.Writer),并可选传入自定义的 handle 函数。
-
Recovery()→ 固定用os.Stderr输出,固定返回 500 响应 -
CustomRecovery(handle)→ 仍用os.Stderr,但把 panic 错误交给你的handle处理 -
RecoveryWithWriter(out, handle)→ 你可以把日志写进文件、写进 lumberjack、甚至丢给 zap,同时还能控制响应内容
例如用文件记录错误日志,就得选 RecoveryWithWriter(fileWriter, myHandle),而不是直接用 Recovery()。
为什么有些 panic 没被 Recovery 捕获到
常见原因不是 Recovery 失效,而是 panic 根本没发生在它的作用域内:
- HTTP server 启动失败(如端口被占)——发生在
r.Run()外,Recovery 完全不参与 - init 函数或 main 中 panic ——还没进 HTTP 请求生命周期
- 在 goroutine 里直接 panic(如
go func(){ panic("xxx") }())——不在当前请求 goroutine,recover()无效 - panic 发生在 Recovery 中间件注册之前——比如你用
gin.New()却忘了Use(Recovery()) - 连接已断开(如客户端关浏览器)后再往
ResponseWriter写数据,触发*net.OpError——Gin 的 Recovery 默认会跳过这类错误的日志,但依然算“捕获”,只是不打印堆栈
生产环境该不该用默认 Recovery
不该。默认 Recovery() 把错误全打到 os.Stderr,且对所有 panic 一视同仁返回 500,缺乏区分度和可控性。
更稳妥的做法是:
- 用
RecoveryWithWriter()把错误写入结构化日志文件(如配合lumberjack.Logger) - 自定义
handle函数,对不同 panic 类型做分级处理:业务校验 panic 返回 400,系统级 panic 才返回 500 - 在 handle 里过滤掉已关闭连接的错误(检查
err是否为*net.OpError并含"broken pipe") - 避免在 handle 里再 panic ——否则 Recovery 自身崩溃,服务就真挂了
真正容易被忽略的,是 Recovery 的作用边界:它只保 HTTP 请求处理链,不保整个进程。panic 发生在哪一层,决定了它能不能被捞起来。


















