go死锁检测器checkdead()仅在所有goroutine均不可运行时panic,它不预测逻辑、不超时判断,只遍历检查状态;cgo存在时可能抑制检测,缓冲channel和goroutine泄漏不会触发,定位需依赖trace/pprof等工具。
go 的死锁检测器不会预测或分析代码逻辑,它只在所有 goroutine 都真实“停摆”时才 panic —— 也就是说,checkdead() 不是守门员,而是收尸人。
checkdead() 触发的唯一条件:全局无 runnable goroutine
运行时每轮调度循环末尾都会调用 checkdead()(位于 src/runtime/proc.go),它不做任何静态检查或超时判断,只做一件事:遍历当前所有 goroutine,看有没有任何一个处于 runnable 或 running 状态。
- 只要有一个 goroutine 处于
runnable(比如刚被唤醒、刚从系统调用返回、或刚被 channel 发送方唤醒),checkdead()就直接返回,什么也不做 - 如果全部 goroutine 都卡在:
ch 、<code>、<code>mu.Lock()、runtime.gopark(含time.Sleep)、未完成的系统调用等阻塞点,且没有一个能被唤醒 → 立即fatal("all goroutines are asleep - deadlock!") - 注意:
select {}是显式永久挂起,它让 goroutine 进入waiting状态,不参与调度,但会被checkdead()当作“已睡着”计入统计
为什么有时必死锁却不 panic?cgo 是最大干扰源
当你 import "net/http" 或使用 os/exec、database/sql(底层依赖 cgo)后,checkdead() 可能完全沉默——哪怕 10 个 goroutine 全卡在 ch 上,主 goroutine 在 <code> 死等,程序也只会挂起,不报错。
- 根本原因:cgo 允许 C 代码通过
pthread异步回调 Go 函数(如网络就绪通知),运行时无法确定“当前没 runnable goroutine”是否真意味着永远卡死,只能保守假设“C 层可能随时唤醒我” - 验证方法:设
CGO_ENABLED=0 go run main.go,若此时 panic 出现,基本可断定是 cgo 干扰了检测 - 常见陷阱:写测试时忘了关 cgo,导致死锁 bug 漏过 CI;线上服务因 DNS 解析(
net.Resolver)隐式启用 cgo,掩盖了 channel 设计缺陷
channel 单 goroutine 顺序读写:最典型的误判盲区
下面这段代码一定会触发 checkdead():
func main() {
ch := make(chan int)
ch <p>但它不是“检测到潜在风险”,而是 runtime 看到:主 goroutine 卡在第一行,整个程序只剩这一个 goroutine,且它不可运行 → 符合触发条件。</p>
- 缓冲 channel 不会立即触发:如
ch := make(chan int, 1); ch 是安全的,因为第一次发送不阻塞 - goroutine 泄漏不算死锁:
go func() { time.Sleep(time.Hour) }()+ 主 goroutineselect {}→ 所有 goroutine 都在 sleep 或 waiting,但checkdead()不 panic,因为 sleep 是可唤醒状态(OS 层面 timer 事件会触发唤醒) - 真正危险的是“假活跃”:比如一个 goroutine 卡在
http.Get(cgo + 阻塞系统调用),它状态是syscall,不被checkdead()认为“已睡着”,但业务上已停滞
别指望 checkdead() 帮你 debug,它只负责终局裁决
checkdead() 不提供位置信息、不打印 goroutine 栈、不区分是 channel 还是 mutex 卡住——它只输出那句著名的 fatal 错误。要定位卡点,得靠其他工具:
-
go tool trace:能看到每个 goroutine 阻塞在哪个ch 或 <code>,适合排查“看似没死锁但不动”的情况 -
pprof.GoroutineProfile:抓取所有 goroutine 栈,过滤出状态为chan receive或chan send的,快速定位 sender/receiver 分布 - 单元测试加
time.AfterFunc:在测试中启动 goroutine 后,用定时器强制 fail,避免因 cgo 或 syscall 导致的漏检
真正容易被忽略的,是那些不触发 checkdead() 却让服务半死不活的情况:比如 WaitGroup 漏掉 Done() 导致 Wait() 永久挂起,或者 RWMutex 的 RUnlock() 调用次数不匹配,使后续 Lock() 卡住——这些都不会 panic,但会让整个模块静默失效。











