“all goroutines are asleep - deadlock!” 看起来是 Go 帮你兜底的死锁检测,但它只在“所有 goroutine 都不可能被唤醒”时才触发。这篇用 Go 1.27.3 复现它:看 recover 能不能抓、9 种常见写法分别报什么状态、哪些“旁观者”会让检测失效、挂住之后怎么看栈、goroutine 泄漏为什么没有报错,以及 go test 里为什么先等到的是超时。
<-ch(退出码 2,用时 0.12 s):
before
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
<tmp>/basic/main.go:7 +0x6c
加 defer recover()(退出码 2,用时 0.11 s):
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
<tmp>/recov/main.go:7 +0x4c读没人写的 channel,fatal error,退出码 2,recover 无效。
从无人写的 channel 读:退出码 2
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
向无人读的无缓冲 channel 写:退出码 2
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
wg.Add(1) 之后没人 Done,wg.Wait():退出码 2
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [sync.WaitGroup.Wait]:
同一个 goroutine 里 Lock 两次:退出码 2
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [sync.Mutex.Lock]:
持有读锁时再请求写锁:退出码 2
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [sync.RWMutex.Lock]:
select {} 没有任何分支:退出码 2
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [select (no cases)]:
for range 一个永远不 close 的 channel:退出码 2
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
读一个 nil channel:退出码 2
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive (nil chan)]:
sync.Cond 没人 Signal:退出码 2
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [sync.Cond.Wait]:第一行都一样,goroutine 头里方括号的词(chan send、sync.Mutex.Lock……)才指出卡在哪类原语。
另一个 goroutine 里 for { time.Sleep(100ms) }:没有检测,被超时杀掉(rc=timeout,3.0 s)
创建了一个没人读的 time.NewTicker:检测到死锁并退出(rc=2,0.1 s)
另一个 goroutine 里 for range ticker.C 在读 ticker:没有检测,被超时杀掉(rc=timeout,3.0 s)
time.AfterFunc 设了一个 1 小时后的定时器:没有检测,被超时杀掉(rc=timeout,3.0 s)
另一个 goroutine 里 net.Listen 后 Accept:没有检测,被超时杀掉(rc=timeout,3.0 s)
signal.Notify 订阅了 SIGUSR1:没有检测,被超时杀掉(rc=timeout,3.0 s)
另一个 goroutine 在空转 for {}:没有检测,被超时杀掉(rc=timeout,3.0 s)有监听、信号、Sleep 循环、被读取的 ticker、未触发的定时器或空转 goroutine 时,运行时认为程序还有机会继续,不报错。
睡 1 s 后结束的 goroutine(退出码 2,用时 1.10 s):
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
…(共 4 行)
2 s 后触发的 AfterFunc(退出码 2,用时 2.10 s):
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
…(共 4 行)定时器触发完、旁观 goroutine 结束后,才会在那一刻报死锁。
发 SIGQUIT 后(退出码 2,用时 1.01 s)里的关键行:
listening
SIGQUIT: quit
goroutine 0 gp=0x102eafce0 m=0 mp=0x102eb04e0 [idle]:
goroutine 1 gp=0x117caa561e0 m=nil [chan receive]:
main.main()
goroutine 2 gp=0x117caa56780 m=nil [force gc (idle)]:
goroutine 17 gp=0x117cab043c0 m=nil [GC sweep wait]:
goroutine 18 gp=0x117cab045a0 m=nil [GC scavenge wait]:
goroutine 3 gp=0x117caa570e0 m=nil [GOMAXPROCS updater (idle)]:
goroutine 4 gp=0x117caa572c0 m=nil [finalizer wait]:
goroutine 5 gp=0x117caa574a0 m=nil [cleanup wait]:
goroutine 6 gp=0x117caa57680 m=nil [IO wait]:kill -QUIT 让进程打印所有 goroutine 的栈,找停在你自己代码上的那几个。
1 万个泄漏的 goroutine(退出码 0,用时 0.42 s):
goroutine 数 1 -> 10001
StackInuse 增加 20128 KiB,平均每个 goroutine 2061 字节
main 正常结束,没有任何报错main 活着,卡住的 goroutine 越积越多也没有任何提示,每个至少占一个最小栈。
go test -timeout 2s,测试里读无人写的 channel(退出码 1):
panic: test timed out after 2s
running tests:
TestDead (2s)
goroutine 19 [running]:
testing.(*M).startAlarm.func1()
created by time.goFunc
goroutine 1 [chan receive]:
testing.(*T).Run(0x2a0b75f34008, {0x1046ada36?, 0x2a0b75eaaa88?}, 0x104817710)
testing.runTests.func1(0x2a0b75f34008)
testing.tRunner(0x2a0b75f34008, 0x2a0b75eaabb8)
…(共 19 行)
go test -timeout 0(关掉超时),限时 8 秒(退出码 1):
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
testing.(*T).Run(0x2ad3326d8008, {0x100ef9a36?, 0x2ad33261aa88?}, 0x101063710)
testing.runTests.func1(0x2ad3326d8008)
testing.tRunner(0x2ad3326d8008, 0x2ad33261abb8)
testing.runTests({0x100ef8f60, 0x1}, {0x100ef8f60, 0x1}, 0x2ad3326a8078, {0x101081538, 0x1, 0x1}, {0x2ad33261ac58?, 0x2ad3326ac180?, ...})
testing.(*M).Run(0x2ad332694140)
main.main()
goroutine 35 [chan receive]:
t.TestDead(0x2ad3326d8248?)
…(共 14 行)默认带超时定时器,先得到 test timed out;关掉超时后才是 fatal 死锁。