Fiber的Recover()中间件默认不捕获子goroutine panic,因其仅作用于当前请求goroutine,而Go中recover只能捕获同goroutine内panic;子goroutine panic不会传播,必须在每个go语句内部显式添加defer recover()。

为什么 Fiber 的 Recover() 中间件默认不捕获子 goroutine panic
Fiber 自带的 middleware.Recover() 只 wrap 当前请求 goroutine 的执行链,和 Gin/Echo 的 recovery 中间件一样,它对 go func() { panic("x") }() 这类子协程完全无感。recover 机制天生绑定 goroutine 生命周期,跨 goroutine 的 panic 不会传播、也不会被外层 defer 捕获——这是 Go 运行时设计,不是 Fiber 的缺陷。
常见翻车现象:Recover() 开了,日志里却看到 panic: send on closed channel 直接终止进程;或者 HTTP 返回 200,但后台 goroutine 已静默退出,定时任务/异步通知从此失联。
- 所有显式启动的 goroutine(包括
go handler()、go task.Run())必须自带defer recover() - 避免在路由 handler 内裸写
go doSomething();改用封装函数,例如:func safeGo(f func()) { go func() { defer func() { if r := recover(); r != nil { log.Printf("panic in goroutine: %v\n%s", r, debug.Stack()) } }() f() }() } - 检查第三方库是否内部起 goroutine(如某些 WebSocket 客户端、消息队列 SDK),它们的 panic 同样逃逸于 Fiber 中间件之外
Fiber 中如何让 panic 日志带 traceID 和完整堆栈
默认 Recover() 只打 panic: xxx,没上下文、没调用栈、无法关联请求。线上排查时等于盲人摸象。
正确做法是:在自定义 recover 中间件里主动注入 traceID(从 c.Locals 或 c.Get("X-Request-ID") 取),并用 debug.Stack() 获取原始堆栈:
立即学习“go语言免费学习笔记(深入)”;
func CustomRecover() fiber.Handler {
return func(c *fiber.Ctx) error {
defer func() {
if r := recover(); r != nil {
// 尝试从上下文取 traceID
traceID := c.Locals("trace_id")
if traceID == nil {
traceID = c.Get("X-Request-ID")
}
log.Printf("[PANIC][%v] %v\n%s", traceID, r, debug.Stack())
c.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
}()
return c.Next()
}
}- 务必在
debug.Stack()前判断r != nil,否则空 panic 会触发空指针 - 不要用
fmt.Sprintf("%+v", r)替代debug.Stack(),前者不包含文件名和行号 - 若用 OpenTelemetry,可将
traceID注入log的context.WithValue,实现日志-链路打通
main 函数里加 defer recover() 有用吗
有用,但作用范围极窄:只兜住 main goroutine 本身的 panic,比如 init() 阶段错误、fiber.New() 参数错、或 app.Listen() 前的逻辑崩溃。它对任何已启动的 HTTP 请求、子 goroutine、定时器回调都无效。
典型误用:把 main 里的 recover 当成“全局兜底”,结果线上一出 panic 还是挂。
- 必须加,但只是防御纵深的第一层,不能替代中间件和
safeGo - 加的位置必须在
app.Listen()之前,且确保defer在main返回前触发 - 示例:
func main() { defer func() { if r := recover(); r != nil { log.Fatalf("PANIC in main: %v\n%s", r, debug.Stack()) } }() app := fiber.New() app.Use(middleware.Recover()) app.Get("/", handler) log.Fatal(app.Listen(":3000")) }
哪些 panic 根本 recover 不了,必须提前预防
recover() 对 runtime 级别致命错误无效:比如 fatal error: all goroutines are asleep - deadlock、fatal error: schedule: spinning with 0 ready queues、或 runtime: out of memory。这些不是 panic,是 Go 运行时直接终止进程,defer 根本没机会跑。
真正能靠 recover 拦住的,仅限业务层主动 panic() 或运行时触发的可恢复 panic(如切片越界、空 map 写入、nil 接口调用)。
- 死锁、内存溢出、栈溢出这类问题,靠日志和监控发现,靠代码审查和压力测试预防
- 对已知高危操作必须前置检查:json.Unmarshal 前校验输入长度、template.Execute 前确认 data 非 nil、channel 操作前用
select { case 避免阻塞 - 第三方库若文档标明“可能 panic”,必须包一层
safeGo或显式if err != nil判断,别信“它自己会处理”
Fiber 的 panic 防护不是配一个中间件就完事,而是每个 goroutine 边界、每个第三方调用点、每个初始化入口都要手动设防。最容易被忽略的是:你以为 recover 了,其实 panic 发生在另一个 goroutine 里,连日志都没留下。


















