← 中文资源

简体中文版本

Go 死锁 — all goroutines are asleep

通过测试超时、SIGQUIT 堆栈与 pprof goroutine 转储,定位 channel、WaitGroup 与互斥锁导致的 Go 死锁。

阅读英文原文

现象

程序打印 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 总数稳定。