Go中协程panic无法自动传播至主goroutine,需在协程内用defer+recover捕获并发送error到channel,主goroutine接收处理;批量场景结合WaitGroup与带缓冲error channel统一收集错误。
在 Go 中,协程(goroutine)内发生的 panic 不会自动传播到启动它的主 goroutine,因此无法用外层 defer + recover 直接捕获。要安全地捕获协程内部错误,需结合 channel 传递错误信息,并在协程内部用 recover 拦截 panic,再将错误发送到 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")
}
当启动多个 goroutine 时,可配合 sync.WaitGroup 确保所有协程结束,并用一个共享的 chan error 收集全部错误(注意 channel 容量或使用带缓冲的 channel,避免发送阻塞)。
make(chan error, n)),或用无缓冲 channel + select 配合超时/关闭机制把 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 <-tim
e.After(3 * time.Second):
log.Println("wait timeout")
return
}
}
实际使用中需注意几个关键点: