该错误仅在所有goroutine均阻塞且无任何runnable状态时触发,是运行时保底panic机制,非静态检测;常见于channel双向阻塞、Mutex重入、WaitGroup未Add/Done等场景。

Go runtime 什么时候会报 fatal error: all goroutines are asleep - deadlock
这个错误只在所有 goroutine 都阻塞且无任何可运行的 goroutine 时触发,不是“检测到潜在死锁”,而是“已经卡死”。它不依赖 go tool trace 或第三方工具,是 Go 运行时最底层的保底机制。
常见触发场景:channel 读写双方都阻塞、sync.Mutex 在持有锁时又尝试加同一把锁(非重入)、sync.WaitGroup 的 Wait() 在没 Add() 或漏 Done() 时永远挂起。
- 仅当程序中**没有一个 goroutine 处于 runnable 状态**才会 panic,哪怕只剩一个 goroutine 卡在
select {}或未关闭的chan上也会触发 - 测试中容易漏掉:用
time.Sleep等待并发完成,但实际 goroutine 已提前退出或 panic,主 goroutine 却还在等 —— 此时 runtime 不认为这是死锁,不会报错,但逻辑已错 -
defer中调用带锁操作、或在panic后试图释放资源,也可能导致锁无法释放,后续 goroutine 卡住
用 go tool trace 定位 channel 阻塞点
比看 panic 更早发现问题:go tool trace 能看到 goroutine 的阻塞位置、channel 缓冲状态、谁在发/谁在收,尤其适合排查“看似没死锁但程序不推进”的情况。
实操步骤:
立即学习“go语言免费学习笔记(深入)”;
- 启动程序前加
import _ "net/http/pprof",并在主函数开头加go http.ListenAndServe("localhost:6060", nil) - 运行后执行
go tool trace http://localhost:6060/debug/trace?seconds=5,下载trace.out - 用
go tool trace trace.out打开 Web UI,在 “Goroutine” 标签页筛选状态为chan receive或chan send的 goroutine,点进去看阻塞在哪个ch <- v或<-ch
注意:go tool trace 本身有性能开销,别在生产环境长期开着;缓冲区为 0 的 channel 最容易暴露阻塞,而带缓冲的 channel 可能只是“暂时满/空”,需结合上下文判断是否真卡死。
sync.Mutex 和 sync.RWMutex 的典型误用
Go 的 mutex 不支持重入,也不自动检测递归锁,一旦在持有锁期间再次 Lock(),就会永久阻塞 —— runtime 不报 deadlock,因为那个 goroutine 还在 running 状态,只是自己卡住了。
- 常见错误:在方法 A 中
mu.Lock(),然后调用同结构体的另一个方法 B,B 内部又mu.Lock()—— 没有 unlock 就进第二次 lock -
RWMutex的RUnlock()必须和RLock()成对,多调或少调都会破坏内部计数器,导致后续Lock()永远阻塞(但不会 panic) - 用
go vet -race能发现部分竞态,但**无法发现单 goroutine 内的重复 lock**,这种只能靠代码审查或单元测试覆盖路径
建议:所有 Lock() 后紧跟 defer mu.Unlock(),避免分支遗漏;若必须嵌套调用,考虑拆成无锁接口,或用 sync.Once 替代部分场景。
测试中模拟并发失败比等待更可靠
写 test 时用 time.Sleep 等 goroutine 结束,本质上是赌调度时机,既不可靠也掩盖真实问题。真正该做的是用 channel 或 sync.WaitGroup 显式同步,并设超时。
- 错误写法:
go doWork(); time.Sleep(100 * time.Millisecond)—— 如果doWork实际耗时 101ms,测试就偶发失败 - 正确做法:用
done := make(chan struct{}),goroutine 结尾写close(done),测试里用select { case - 如果涉及多个 goroutine,优先用
sync.WaitGroup,但务必确保wg.Add()在go之前,且每个分支都有defer wg.Done(),否则wg.Wait()会永远挂起
最容易被忽略的一点:context.WithTimeout 包裹的 channel 操作,如果 context 被 cancel,接收方可能拿到零值或直接跳过,但发送方若没用 select 做非阻塞 send,仍可能卡在 ch <- v 上 —— 这类问题不会立刻 panic,却让 goroutine 积压,最终拖垮整个服务。










