自定义 recover 中间件必须用 defer 包裹 recover():因 recover() 仅在 panic 后、栈未完全展开时于 defer 内直接调用才有效,普通位置调用恒返回 nil;需确保 defer 在 c.next() 前声明、recover 在 defer 第一行、panic 发生于 c.next() 链中,且避免前置响应、顺序错误、goroutine 泄露及忽略堆栈日志。

别用 fiber.Default() 自带的 Recover 中间件——它在生产环境会泄露敏感头、拖慢 TTFB 0.3–0.8ms,且无法控制执行顺序;自己手写一个才可控、可审计、可上报。
为什么自定义 Recover 必须用 defer 包裹 recover()
recover() 不是监听器,是栈展开拦截器:只有在 panic 发生后、当前 goroutine 栈尚未完全展开时,于 defer 函数内直接调用才有效。普通位置调用恒返回 nil。
常见错误写法:
func Recovery() fiber.Handler {
return func(c *fiber.Ctx) error {
// ❌ 错误:这里 recover() 永远是 nil
if r := recover(); r != nil {
c.Status(500).JSON(rsp.FailMsg("internal error"))
return nil
}
return c.Next()
}
}
正确结构必须是:
-
defer块在c.Next()之前声明,且包裹整个业务链 -
recover()必须出现在defer函数体第一行(避免被其他语句干扰) - panic 必须发生在
c.Next()执行期间或其调用链中,才能被捕获
ctx.Next() 前后写响应会导致 Recover 失效
如果中间件或 handler 在 c.Next() 之前就调用了 c.JSON()、c.Send() 或 c.Status(401).Send(),那么后续 c.Next() 实际不会执行,defer 虽然仍会运行,但 panic 已不在该作用域内——等于没挂载业务逻辑。
典型翻车场景:
- 权限中间件里判断失败后写了
c.Status(401).Send("unauthorized"),又跟一句c.Next()→ 响应已发,Fiber 终止链路,defer里的recover()完全收不到 panic - 把可能 panic 的解包逻辑(如
json.Unmarshal(c.Body(), &v))写在c.Next()前面 → panic 发生时还没进defer作用域 - 注册顺序错:比如
app.Use(Recovery())写在app.Use(BasicAuth())后面 → BasicAuth 内部 panic 直接逃逸
goroutine 里的 panic 永远不会被外层 Recover 拦截
Fiber 的 Recover 中间件只对当前 HTTP handler goroutine 生效。任何你用 go func() { ... }() 启的子 goroutine,里面的 panic 都不会触发中间件里的 recover() —— 连日志都不会打,连接静默断开。
必须显式处理:
- 每个子 goroutine 都要自己加
defer func() { if r := recover(); r != nil { log.Printf("panic in goroutine: %+v", r) } }() - 更推荐封装一个
safeGo()工具函数,自动包recover+debug.Stack()+ 上报(比如 Sentry) - 不要依赖全局 Recover 兜底——这是 Go 的设计约束,不是框架缺陷
recover 后别只打印 panic 消息,至少要 debug.Stack()
仅 fmt.Printf("panic: %v", r) 只能拿到 panic 值,找不到具体哪行代码崩的。真正有用的线索在堆栈里。
建议 recover 分支里固定包含:
log.Printf("panic recovered: %+v\n%s", r, debug.Stack())- 若使用统一响应结构(如
rsp.FailMsg()),状态码必须显式设为500,不能只靠字符串内容推断 - 别用
http.Error()混入响应流——它和c.AbortWithStatusJSON()行为不同:c.AbortWithStatusJSON()会c.Abort()阻止后续中间件,而http.Error()不会,容易导致前端解析混乱
最常被忽略的一点:recover 不是兜底银弹。业务错误(参数非法、资源不存在)不该用 panic,而要用 return nil, errors.New("xxx") 或自定义 *AppError;真正该 recover 的,是 map 写入未初始化、第三方库内部崩溃、模板渲染时 reflect.Value.Interface() 失败这类不可预期的底层问题。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











