recover必须在当前goroutine的defer函数中调用才有效,直接调用或跨goroutine调用均返回nil;它仅捕获本协程内未被其他recover处理的panic,且捕获后函数立即return,不恢复执行流。

为什么不能直接用 recover() 捕获 goroutine 的 panic
因为 recover() 只在当前 goroutine 的 defer 链中有效,且必须在 panic 发生的同一 goroutine 中调用。启动新 goroutine 后,它的调用栈和 defer 链完全独立,主 goroutine 的 recover() 对它毫无作用。
常见错误是这样写:
go func() {
// 这里 panic,但外面 recover 不到
panic("boom")
}()
defer func() {
if r := recover(); r != nil { // 永远不会触发
log.Println("caught:", r)
}
}()结果就是 panic 未被捕获,程序崩溃或输出 fatal error: all goroutines are asleep - deadlock(如果还等它结束的话)。
标准做法:每个 goroutine 内部加 defer + recover()
最直接、最可控的方式,就是在每个需保护的 goroutine 函数体开头就安排 defer 和 recover()。
立即学习“go语言免费学习笔记(深入)”;
- 必须在 goroutine 启动前就定义好带 recover 的闭包,不能事后补
- 推荐封装成工具函数,避免重复代码
- 注意:recover 后程序继续运行,但该 goroutine 已退出,别试图“恢复执行”
示例:
func safeGo(f func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("goroutine panicked: %v", r)
}
}()
f()
}()
}
<p>// 使用
safeGo(func() {
time.Sleep(100 * time.Millisecond)
panic("in goroutine")
})用 sync.WaitGroup + 错误通道统一收集 panic 信息
当需要批量启动 goroutine 并汇总所有 panic 错误时,光靠日志不够——你可能想做失败重试、统计或上报。这时要配合 sync.WaitGroup 和一个带缓冲的 chan error。
- 缓冲区大小建议设为预期 goroutine 数量,避免发送阻塞导致 goroutine 卡住
- 务必在 goroutine 内关闭 channel 或用
sync.Once控制,否则主 goroutinerange会永远等待 - 不要在 recover 后往同一个无缓冲 channel 发送,极易死锁
示例:
errCh := make(chan error, 10)
var wg sync.WaitGroup
<p>for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
defer func() {
if r := recover(); r != nil {
errCh <- fmt.Errorf("worker %d panicked: %v", id, r)
}
}()
if id == 2 {
panic("simulated failure")
}
time.Sleep(50 * time.Millisecond)
}(i)
}</p><p>go func() {
wg.Wait()
close(errCh)
}()</p><p>for err := range errCh {
log.Println("error:", err)
}进阶:用第三方库 errgroup.Group 简化管理
golang.org/x/sync/errgroup 提供了带错误传播和上下文取消的 goroutine 批量控制能力,但它本身不自动 recover panic——你需要手动包装。
- 它只捕获显式返回的
error,对 panic 仍需自己 defer recover - 适合已习惯用
errgroup做并发控制的项目,避免混用多种模式 - recover 后建议把 panic 转成 error 返回,以便
eg.Wait()统一处理
示例:
eg, ctx := errgroup.WithContext(context.Background())
for i := 0; i < 3; i++ {
i := i
eg.Go(func() error {
defer func() {
if r := recover(); r != nil {
// 转为 error,让 errgroup 捕获
eg.TryGo(func() error {
return fmt.Errorf("panic in worker %d: %v", i, r)
})
}
}()
if i == 1 {
panic("from errgroup worker")
}
return nil
})
}
if err := eg.Wait(); err != nil {
log.Println("errgroup finished with error:", err)
}真正容易被忽略的是 panic 的类型多样性:recover() 返回的是 interface{},可能是 string、error、自定义 struct,甚至 nil(比如调用了 runtime.Goexit())。不做类型断言直接打印,可能得到 <nil> 或难读的内存地址。线上环境建议统一转成字符串并带上堆栈,用 debug.PrintStack() 或 runtime/debug.Stack() 补充上下文。


















