贝利信息

Golang并发场景下的错误收集方案

日期:2026-01-09 00:00 / 作者:P粉602998670
goroutine 中未捕获 panic 会导致程序崩溃,需在每个 goroutine 入口用 defer/recover 捕获并记录堆栈;并发写入同一 error slice 会引发竞态,应使用 errgroup.Group 或加锁保护。

goroutine 中 panic 未捕获导致程序崩溃

Go 的 goroutine 是轻量级线程,但它的 panic 不会向主 goroutine 传播。一旦某个 go 启动的函数 panic 且未 recover,整个程序就直接退出,错误信息还可能被吞掉。

常见现象是:日志里没看到 panic 日志,服务却突然挂了;或者只在高并发压测时偶发崩溃,本地复现困难。

go func() {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("panic in goroutine: %v\n%v", r, string(debug.Stack()))
        }
    }()
    // 业务逻辑
}()

多个 goroutine 并发写入同一 error slice 导致数据丢失

很多人习惯用 []error 收集子任务错误,但在并发下直接 append 会引发竞态——append 可能触发底层数组扩容,多个 goroutine 同时写入同一 slice 头部字段(len/cap)就会出错。

典型错误信息:fatal error: concurrent map writes(虽然写的是 slice,但底层行为类似)或静默丢数据。

g, ctx := errgroup.WithContext(context.Background())
for i := range tasks {
    i := i
    g.Go(func() error {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
            return doTask(tasks[i])
        }
    })
}
if err := g.Wait(); err != nil {
    log.Printf("first error: %v", err)
}

context.Cancelled 被误判为业务错误

在超时或取消场景下,context.Canceledcontext.DeadlineExceeded 是控制流信号,不是真正的失败。如果统一把它们和其他 error 一起收集上报,会导致监控误报、告警轰炸。

尤其在使用 errgroupsync.WaitGroup + 手动 error channel 时,容易把 cancel 错当业务异常。

error channel 缓冲不足引发 goroutine 泄漏

chan error 收集错误很常见,但如果 channel 无缓冲且接收端未及时读取,发送 goroutine 就会永久阻塞——尤其在部分任务快速失败、其他任务还在运行时,泄漏的 goroutine 会越积越多。

现象是:goroutine 数持续上涨,pprof 查看大量 goroutine 停在 chan send

errs := make(chan error, len(tasks))
for _, task := range tasks {
    go func(t Task) {
        if err := t.Run(); err != nil {
            select {
            case errs <- err:
            default:
                // 忽略或打日志,避免阻塞
                log.Printf("error channel full, drop: %v", err)
            }
        }
    }(task)
}

实际中最容易被忽略的,是 recover 的作用域边界和 error channel 的生命周期管理——它们不像 HTTP handler 那样有明确框架兜底,全靠开发者自己卡住 goroutine 的入口和出口。