答案是:死锁主因是channel收发不匹配或锁未释放,需用dlv冻结现场查goroutines -t和goroutine bt定位阻塞锚点。看到所有goroutine卡在chan send/recv或sync.(*mutex).lock时,重点检查无缓冲channel是否缺配对操作、rwmutex是否重入读锁、或关闭逻辑缺失。

dlv 调试时 goroutine 全部卡在 chan send 或 chan recv
看到 fatal error: all goroutines are asleep - deadlock! 后立刻停手,别改代码重跑。dlv 的价值在于「冻结现场」——此时所有 goroutine 状态都还活着,堆栈没丢。
启动调试:用 dlv debug ./yourbinary 进入交互模式,输入 continue 让程序运行到 panic 前一刻(dlv 会自动中断在死锁检测点)。
关键命令只有两个:goroutines 列出全部 goroutine 及状态;goroutine <id> bt</id> 查看指定 goroutine 的完整调用栈。
重点看哪些 goroutine 的堆栈末尾是:runtime.chansend、runtime.chanrecv、sync.(*Mutex).Lock 或 sync.(*RWMutex).RLock ——这些就是阻塞锚点。
为什么 goroutines 列表里看不到 main goroutine?
不是消失了,是它被调度器标记为 dead 或 idle,默认不显示。必须加 -t 参数:goroutines -t 才能强制列出所有,包括已停止但未清理的。
常见误判:看到一堆 goroutine 卡在 chan send,就以为是 channel 问题。但如果其中多个 goroutine 的调用栈里反复出现 sync.(*RWMutex).RLock,尤其在同一行函数内嵌套调用,大概率是 RWMutex 读锁重入导致的隐式死锁。
注意:RWMutex 不允许同 goroutine 多次 RLock(),也不允许已持读锁再调 Lock()。标准库不会报错,但内部计数错乱后,其他 goroutine 就永远等不到锁释放。
如何确认是 channel 收发不匹配而非逻辑卡死?
看阻塞 goroutine 的 channel 类型和操作上下文:
• 如果是无缓冲 channel(make(chan int)),且只有一个 goroutine 在 ch ,其余全在 <code> 或空 select,基本可断定发送方没人接;<br>• 如果是带缓冲 channel(<code>make(chan int, 10)),但堆栈显示 runtime.chansend 且 len(ch) == cap(ch),说明缓冲区已满,得查谁该收不收;
• 若多个 goroutine 分别卡在 ch1 和 <code>,而它们的调用栈又交叉指向对方的 channel 操作,就是典型的循环等待死锁;<br>• 出现 <code>select {} 或 for {} 且无任何 channel 操作,说明协程漏了退出条件,不是资源争用,但同样触发死锁 panic。
dlv 中无法复现死锁?试试 GODEBUG=schedtrace=1000
有些死锁依赖特定调度时机,dlv 单步或断点可能改变执行流,导致无法触发。这时要绕过调试器,用运行时调度跟踪:
在终端执行:GODEBUG=schedtrace=1000 go run main.go,每秒输出一次调度快照;
关注两行关键信息:SCHED 行里的 G 数(goroutine 总数)是否长期为 0,以及 runqueue 是否持续为空;
如果 G 数骤降到 0 且不再回升,说明所有 goroutine 都已阻塞,死锁已发生;
配合 go tool trace 生成 trace 文件,用浏览器打开后聚焦在「Goroutines」视图,能直观看到哪些 goroutine 长时间处于 Running → Blocked 状态,并直接跳转到对应源码行。
实际调试中最容易忽略的是:死锁 panic 日志里打印的 goroutine 列表不是参考信息,是唯一真相。它不撒谎,也不推测——谁在等什么、等了多久、从哪一行开始卡住,全在里面。别急着翻代码逻辑,先把它逐行读完。











