Go中协程panic无法自动传播至主goroutine,需在协程内用defer+recover捕获并发送error到channel,主goroutine接收处理;批量场景结合WaitGroup与带缓冲error channel统一收集错误。

在 Go 中,协程(goroutine)内发生的 panic 不会自动传播到启动它的主 goroutine,因此无法用外层 defer + recover 直接捕获。要安全地捕获协程内部错误,需结合 channel 传递错误信息,并在协程内部用 recover 拦截 panic,再将错误发送到 channel 中供主流程处理。
协程内用 recover 捕获 panic 并写入 error channel
核心思路是:每个可能 panic 的 goroutine 内部都包裹一层 defer + recover,把捕获到的错误(或非 nil 的 recover 结果)转为 error 类型,通过预先创建的 chan error 发送出去。主 goroutine 则从该 channel 接收并统一处理。
示例:
func doWork() error {
// 模拟可能 panic 的操作
if rand.Intn(10) == 0 {
panic("something went wrong")
}
return nil
}
func runWithRecover(errCh chan<- error) {
defer func() {
if r := recover(); r != nil {
var err error
switch v := r.(type) {
case string:
err = fmt.Errorf("panic: %s", v)
case error:
err = fmt.Errorf("panic: %w", v)
default:
err = fmt.Errorf("panic: unknown type %v", v)
}
errCh <- err
}
}()
// 实际业务逻辑
if err := doWork(); err != nil {
errCh <- err
return
}
}
// 使用方式
errCh := make(chan error, 1) // 缓冲 1 避免 goroutine 阻塞
go runWithRecover(errCh)
select {
case err := <-errCh:
if err != nil {
log.Printf("got error: %v", err)
}
case <-time.After(5 * time.Second):
log.Println("timeout")
}
批量协程错误收集:用 WaitGroup + channel 统一收口
当启动多个 goroutine 时,可配合 sync.WaitGroup 确保所有协程结束,并用一个共享的 chan error 收集全部错误(注意 channel 容量或使用带缓冲的 channel,避免发送阻塞)。
立即学习“go语言免费学习笔记(深入)”;
- 定义容量足够的 error channel(如
make(chan error, n)),或用无缓冲 channel + select 配合超时/关闭机制 - wg.Done(),并在 defer 中 recover 后发送错误
- wg.Wait() 后关闭 error channel,再遍历读取所有错误
封装成可复用的 runner 工具函数
把 recover + channel 模式抽象为通用函数,提升复用性:
func GoWithRecover(f func() error, errCh chan<- error) {
go func() {
defer func() {
if r := recover(); r != nil {
var err error
switch v := r.(type) {
case string:
err = fmt.Errorf("panic: %s", v)
case error:
err = fmt.Errorf("panic: %w", v)
default:
err = fmt.Errorf("panic: %v", v)
}
errCh <- err
}
}()
if err := f(); err != nil {
errCh <- err
}
}()
}
// 使用
errCh := make(chan error, 10)
GoWithRecover(doWork, errCh)
GoWithRecover(anotherWork, errCh)
// 收集结果(可加超时)
for i := 0; i < 2; i++ {
select {
case err := <-errCh:
if err != nil {
log.Println("task failed:", err)
}
case <-time.After(3 * time.Second):
log.Println("wait timeout")
return
}
}
注意事项与常见陷阱
实际使用中需注意几个关键点:
- channel 必须有足够缓冲,否则 goroutine 在 send 时会永久阻塞(尤其 recover 后发送错误)
- recover 只对当前 goroutine 有效,不能跨 goroutine 捕获
- 不要在 recover 后忽略错误——即使只打印日志,也要确保错误被消费,否则 channel 可能积压或死锁
- 若业务逻辑本身返回 error,应优先用显式错误处理;recover 仅用于兜底捕获未预期 panic(如空指针、越界等)










