recover() 只能在同 goroutine 的 defer 中捕获 panic,外部无法拦截;SafeGo 必须在新 goroutine 内部封装 defer recover(),并配合日志与错误处理,避免掩盖真实问题。

为什么直接用 recover() 在 goroutine 里不起作用
因为 recover() 只能在 defer 函数中、且 panic 发生在同一线程(goroutine)内时才有效。如果在新起的 goroutine 里 panic,而没在它的内部做 defer recover(),那 panic 就会直接崩溃整个程序——外部 goroutine 是捕获不到的。
常见错误写法:
go func() {
// 这里 panic,但外面没 defer,recover 失效
panic("boom")
}()
这种写法等同于放任崩溃。
- 必须把
defer recover()放在实际执行业务逻辑的 goroutine 内部 - 不能指望主 goroutine 或上层函数替它 recover
- recover 后要显式处理错误(比如 log),否则等于静默吞掉 panic,难排查
SafeGo 函数怎么封装才真正安全
核心是:启动 goroutine 的同时,把 recover 逻辑和原始函数包裹进同一匿名函数体内。这样 panic 发生时,defer 能及时响应。
一个最小可用实现:
func SafeGo(f func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic recovered in goroutine: %v", r)
}
}()
f()
}()
}
- 传入的
f必须是无参无返回值函数;若需传参,用闭包捕获,注意变量逃逸和生命周期 - log 输出应包含堆栈(可用
debug.PrintStack()),否则只打印 panic 值很难定位 - 不要在 recover 后继续执行原逻辑——panic 已破坏当前 goroutine 状态,继续跑大概率二次 panic
带参数和错误回调的增强版 SafeGo
实际开发中常需要传参、捕获 panic 类型、或做自定义上报。这时可扩展签名:
func SafeGoWithRecover(f func(), onPanic func(interface{})) {
go func() {
defer func() {
if r := recover(); r != nil {
if onPanic != nil {
onPanic(r)
} else {
log.Printf("panic: %v", r)
}
}
}()
f()
}()
}
-
onPanic回调能区分业务 panic(如errors.New("timeout"))和意外 panic(如 nil pointer),前者可能不需告警 - 若函数有返回值(比如
func() (int, error)),不能直接套用——recover 只能拦截 panic,无法获取返回值;此时建议改用同步调用 + context 控制超时 - 避免在
onPanic里再起 goroutine,否则可能引发嵌套 panic 或资源泄漏
哪些场景不该用 SafeGo
它不是万能兜底,滥用反而掩盖问题。
- 高频 panic 的代码(比如反复访问未初始化 map)——该修 bug,不该靠 recover 挡着
- 涉及资源释放的逻辑(如文件句柄、数据库连接),recover 后无法保证 defer 已执行,资源可能泄漏
- 单元测试里用
SafeGo捕获 panic 来“验证失败”,这会让测试失去对 panic 位置的精确感知,应直接调用并用test.Panic断言
真正值得 recover 的,通常是外部不可控因素:第三方库 panic、网络回调里解包未知数据、插件动态加载失败等。

















