现象
程序打印 fatal error: all goroutines are asleep - deadlock! 并以状态 2 退出;go test 挂起直到 panic: test timed out;HTTP handler 永不返回,但其他请求仍能处理;goroutine 数持续增长。
诊断
用测试超时拿到完整堆栈,并打开全部 goroutine:
go test -timeout 15s -run TestCheckout ./internal/checkout
GOTRACEBACK=all go test -timeout 15s ./...
go vet ./...
go test -race -timeout 60s ./...
堆栈里找 chan send、chan receive、sync.runtime_SemacquireMutex,读顶部帧即可定位互相等待的一对。线上服务通过 pprof 抓两次转储:
curl -s 'http://localhost:6060/debug/pprof/goroutine?debug=2' | head -80
两次都停在同一个帧的 goroutine 不是慢,而是卡死。go vet 能发现 copylocks 等问题,race 检测器常能暴露锁顺序不一致的根源。注意运行时只在所有 goroutine 都阻塞时才报死锁,部分死锁只表现为挂起。
根因
所有能推进的 goroutine 都在等待一个永远不会运行的对端。只要还有 ticker 或 metrics 协程在跑,同样的问题就不会触发死锁检测,而是变成静默挂起。
修复
保证发送前接收方已启动,或使用按生产者数量设置容量的缓冲 channel;wg.Add 必须在启动 goroutine 之前,结果 channel 由生产者负责关闭:
func process(ctx context.Context, jobs <-chan Job) error {
var wg sync.WaitGroup
for j := range jobs {
wg.Add(1)
go func(j Job) {
defer wg.Done()
handle(ctx, j)
}(j)
}
done := make(chan struct{})
go func() { wg.Wait(); close(done) }()
select {
case <-done:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
sync.Mutex 不可重入,同一 goroutine 重复 Lock 会永久阻塞;多把锁要统一加锁顺序。加缓冲只是推迟死锁,不能替代修复。
验证
反复执行 go test -race -timeout 60s ./... 均能完成;压测时相隔一分钟抓两次 goroutine 转储,没有 goroutine 持续停在 chan send、chan receive 或 semacquire,goroutine 总数稳定。