死锁时panic日志直接显示goroutine堆栈,需重点查看main goroutine停顿位置、channel阻塞点及互斥锁持有者,快速定位同步异常根源。

看 panic 日志里的 goroutine 堆栈
Go 程序一死锁,立刻 panic 并打印 fatal error: all goroutines are asleep - deadlock!,不是卡顿,是彻底停摆。这时候第一反应不是改代码,而是看 panic 输出的 goroutine 列表——它直接告诉你谁在等什么。
重点关注三类线索:
-
maingoroutine 停在哪一行?比如卡在c 、<code>、<code>wg.Wait()或mu.Lock() - 其他 goroutine 是否全卡在同一操作上?比如全部停在
chan send,说明没人收;全停在chan recv,说明没人发 - 有没有 goroutine 卡在
select {}或空for {}?那是忘了退出条件,不是资源争用,但也会触发死锁
日志被截断时,加环境变量运行:GODEBUG=schedtrace=1000 go run main.go,每秒输出调度快照,观察 G 数量是否长期为 0。
查无缓冲 channel 的收发是否成对且有关闭方
无缓冲 channel 是死锁高发区:发送前必须有接收方就位,否则发送方永久阻塞;for v := range ch 前必须有人 close(ch),否则无限等待。
立即学习“go语言免费学习笔记(深入)”;
典型错误场景:
- 单个 goroutine 里先
c 再 <code> → 必死锁 go func() { c 后紧接 <code>x, y := → 第二个 <code> 永远等不到值- 多个 goroutine 往同一 channel 发数据,但只由一个发完就
close(ch)→ 其他发者 panic:send on closed channel
修复关键:谁负责发完所有数据,谁负责 close(ch);多个发送方时,用 sync.WaitGroup 协调,而不是靠 defer 在函数入口 close。
盯住 sync.RWMutex 的读锁重入和锁内 channel 操作
sync.RWMutex 不允许同一个 goroutine 多次 RLock(),也不允许已持读锁时再调 Lock()。这种“看似安全”的写法会破坏内部计数,让其他 goroutine 永久等待。
Web仪表盘,用于审批OpenClaw设备与通道配对、管理连接,并可从浏览器访问实时终端。
常见于缓存模块中:一个方法用 RLock() 读数据并启动 goroutine 发送 channel,而该 goroutine 内部又调了同一把锁的 RLock()。
更隐蔽的坑是:在 mu.Lock() 临界区内做 c 或 <code>。一旦 channel 阻塞(比如无缓冲且无人收),锁就一直不释放,其他 goroutine 全卡在 <code>mu.Lock() 上。
修复方式:
- 每个 goroutine 对同一把
RWMutex最多一次RLock()+ 一次RUnlock() - 临界区只做内存操作,把 IO、channel 通信、网络调用移出锁外
- 测试期可用
go-deadlock替换原生sync,提前暴露重入问题
用 go tool trace 定位谁在等谁
pprof 只能看到栈帧,go tool trace 能还原 goroutine 状态变迁,一眼看出谁在等谁、等了多久、是否永久阻塞。
操作步骤:
- 启动时加
import _ "net/http/pprof",并在main开头起服务:go http.ListenAndServe("localhost:6060", nil) - 运行后访问
http://localhost:6060/debug/trace?seconds=5下载trace.out - 执行
go tool trace trace.out,打开 Web UI,在 “Goroutine” 标签页筛选状态为chan receive或chan send的 goroutine
典型线索:某 goroutine 长时间处于 chan recv 状态,且无对应 sender 唤醒,基本就是死锁源头。注意:无缓冲 channel 最容易暴露阻塞;带缓冲的 channel 可能只是“暂时满/空”,得结合上下文判断。
真正容易被忽略的是关闭 channel 的时机和责任归属——不是“谁创建谁关闭”,而是“谁负责发送完谁关闭”。一旦多个 goroutine 都可能往同一 channel 发数据,就得用 sync.WaitGroup 或 context 协调关闭动作,否则还是会在某条路径上漏掉 close()。

















