如何在Golang中捕获协程内错误_结合channel和recover实现

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,避免发送阻塞)。

  • 定义容量足够的 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(如空指针、越界等)