goroutine 中未捕获 panic 会导致程序崩溃,需在每个 goroutine 入口用 defer/recover 捕获并记录堆栈;并发写入同一 error slice 会引发竞态,应使用 errgroup.Group 或加锁保护。

goroutine 中 panic 未捕获导致程序崩溃
Go 的 goroutine 是轻量级线程,但它的 panic 不会向主 goroutine 传播。一旦某个 go 启动的函数 panic 且未 recover,整个程序就直接退出,错误信息还可能被吞掉。
常见现象是:日志里没看到 panic 日志,服务却突然挂了;或者只在高并发压测时偶发崩溃,本地复现困难。
- 必须在每个独立 goroutine 入口加
defer/recover,不能依赖外层包裹 - recover 后建议记录完整堆栈:
debug.PrintStack()或runtime/debug.Stack() - 避免在 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,但底层行为类似)或静默丢数据。
立即学习“go语言免费学习笔记(深入)”;
- 改用带锁的收集器,比如
sync.Mutex+append - 更推荐用
errgroup.Group,它原生支持并发错误聚合,并自动等待所有 goroutine 完成 - 注意
errgroup.WithContext返回的Group在第一个 error 出现后默认取消其余任务(可选)
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.Canceled 或 context.DeadlineExceeded 是控制流信号,不是真正的失败。如果统一把它们和其他 error 一起收集上报,会导致监控误报、告警轰炸。
尤其在使用 errgroup 或 sync.WaitGroup + 手动 error channel 时,容易把 cancel 错当业务异常。
- 显式判断错误类型:
errors.Is(err, context.Canceled)或errors.Is(err, context.DeadlineExceeded) - 业务函数返回 error 前,先检查
ctx.Err(),如果是则直接返回,避免掩盖真实原因 - 错误收集器中过滤掉 context 相关 error,或单独归类(如统计 cancel 次数而非计入失败率)
error channel 缓冲不足引发 goroutine 泄漏
用 chan error 收集错误很常见,但如果 channel 无缓冲且接收端未及时读取,发送 goroutine 就会永久阻塞——尤其在部分任务快速失败、其他任务还在运行时,泄漏的 goroutine 会越积越多。
现象是:goroutine 数持续上涨,pprof 查看大量 goroutine 停在 chan send。
- 给 error channel 设置合理缓冲:
make(chan error, len(tasks)) - 更稳妥的做法是用
select配合 default 分支,避免阻塞: - 确保有 goroutine 持续从 channel 读取,或用
sync.WaitGroup等待所有发送完成后再关闭 channel
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 的入口和出口。










