recover()只对当前goroutine有效,子goroutine panic无法被外层recover捕获;必须在每个子goroutine内部显式使用defer+recover兜底,否则将静默崩溃。

子goroutine panic 为什么 recover() 捕不到
因为 recover() 只对**当前 goroutine** 有效。你在主 handler 或 main 函数里写 defer func() { recover() }(),只能拦住那个 goroutine 自己的 panic;子 goroutine(比如 go doSomething() 启动的)一 panic,就直接崩溃退出,外层完全无感。
常见错误现象包括:日志里没看到 panic 记录、HTTP 请求没返回却悄悄卡住、后台任务静默停止、sync.WaitGroup 死等不返回。
- HTTP 中间件(如
Gin.Recovery())只 wrap 主请求 goroutine,不 cover 子 goroutine -
debug.SetPanicOnFault(true)对业务 panic(空 map 写入、panic("xxx"))完全无效 - 用
signal.Notify拦SIGABRT也走不通——Go 默认 panic 不发信号
每个 go 语句都得自己包一层 defer recover
没有“全局”捕获这回事,唯一可靠的方式是:**所有显式启动的 goroutine,内部必须自带 defer + recover()**。漏掉一个,就是静默崩溃点。
实操建议:
- 别写
defer recover()——语法错误,recover必须在defer的函数体里调用 - 把
defer放在可能 panic 的代码之前,否则注册不上 - 用匿名函数包裹,避免内联优化导致
recover()返回nil - 示例写法:
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in goroutine: %v", r)
// 推荐加 runtime.Stack 获取 trace
buf := make([]byte, 4096)
runtime.Stack(buf, false)
log.Printf("stack: %s", buf)
}
}()
riskyOperation()
}()
用 safeGo 封装裸 go 调用
手动每次写 go func() { defer ... }() 容易漏、难维护。推荐统一用封装函数兜底:
示例:
func safeGo(f func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in goroutine: %v", r)
buf := make([]byte, 4096)
runtime.Stack(buf, false)
log.Printf("stack: %s", buf)
}
}()
f()
}()
}
使用时直接替换裸 go:
- ❌
go handleEvent(e) - ✅
safeGo(func() { handleEvent(e) })
注意:如果 handleEvent 需要参数,记得用局部变量捕获(如 e := e),避免闭包引用问题。
recover 后不能继续原逻辑,状态很可能已损坏
recover() 只是让 goroutine 不退出,它不会回滚变量、不会重置 map/slice 状态、也不会跳回 panic 那行继续执行。常见误用是 recover 后直接往下走,结果数据错乱或逻辑跳变。
例如:
- panic 前已往 channel 发了半条消息 → recover 后又发一次 → 消费方收到重复/断裂数据
- panic 前修改了某个全局计数器 → recover 后没修复 → 后续统计失真
- panic 发生在事务中间 → recover 后没 rollback → 数据库状态不一致
真正该做的:记录 panic + stack trace,然后做必要清理(close(ch)、http.CloseNotifier() 等),再彻底退出该 goroutine。别试图“续上”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











