bigywater.tech
常见报错 · 41

Go fatal error: all goroutines are asleep - deadlock!:recover 抓不到,有 Accept、Sleep 循环或 ticker 读取在场时检测不到,泄漏的 1 万个 goroutine 不报错

“all goroutines are asleep - deadlock!” 看起来是 Go 帮你兜底的死锁检测,但它只在“所有 goroutine 都不可能被唤醒”时才触发。这篇用 Go 1.27.3 复现它:看 recover 能不能抓、9 种常见写法分别报什么状态、哪些“旁观者”会让检测失效、挂住之后怎么看栈、goroutine 泄漏为什么没有报错,以及 go test 里为什么先等到的是超时。

2
退出码;它是 fatal error 不是 panic,defer recover() 抓不到
6 / 7
给 main 加的 7 种旁观者里有 6 种让死锁检测失效(只有没人读的 ticker 不影响)
2061
泄漏的 goroutine 平均占用的栈字节数:1 万个共约 20 MB,且没有任何报错

最简单的死锁

实测 · 本机执行输出
  <-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 里

实测 · 本机执行输出
  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 死锁。

分步演示

死锁检测
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64;报错文本都是真实复现时原样捕获的(命令行工具统一用 LC_ALL=C 取英文原文,路径换成 );所有服务都是本机上自己起的、只监听 127.0.0.1,不访问外网;计时用 time.perf_counter,数字每次运行会有小幅抖动。本篇用 go1.27.3 darwin/arm64;每个程序是 go build 后直接运行的小 main.go,GOPROXY=off,不联网;“检测不到”的程序限时 3 秒后被杀掉
    • 没有验证:没有读 Go 运行时源码,所以“没人读的 ticker 不影响检测、有人读的 ticker 会影响”只是实测现象,原因(可能与新版本里定时器通道的实现有关)没有确认;其它 Go 版本的行为(旧版本里没人读的 ticker 是否也算旁观者);cgo 程序和 Windows/Linux 上的行为;-race 模式下的输出;用 context 超时、errgroup 或 goleak 这类工具来防止挂住的做法没有测
    京ICP备2026067502号-1京公网安备11010502063487号