Channel 死锁场景与排查¶
要点卡¶
30 秒回答
Go runtime 能报告的是进程已无可继续运行路径的一类全局死锁,典型报错为
fatal error: all goroutines are asleep - deadlock!;局部死锁(部分 G 永久阻塞,
但还有 G、timer、网络/cgo 等活动路径)不会自动报。main 返回会直接结束进程,不是死锁。
生产关键词:无缓冲握手顺序、忘记 close、循环等待、取消路径缺失。
3 分钟展开
- 是什么:若干 G 在 channel 操作上互相等待,无进展。
- 为什么:CSP 同步语义要求配对;缓冲满/空、无接收者、无发送者都会 park。
- 怎么做:设计固定角色(生产者 close)、超时/ctx、buffer、多路 select;避免循环依赖等待链。
| 记忆槽 | 内容 |
|---|---|
| 三个不变量 | channel 操作要有配对方或可用容量;runtime 只检测部分全局无进展状态;关闭权必须由协议指定 |
| 手画图 | G1 send A → 等 G2、G2 send B → 等 G1 画成环,再补 ctx.Done() 退出边 |
| 项目落点 | 用实际链事件流水线说明 producer、worker、collector 的关闭所有权和取消路径;只引用本人实际参与部分和可解释指标 |
| 一个取舍 | buffer 可吸收短时错峰但不能修复循环依赖;越大越可能把错误推迟到生产高峰 |
错误表达
- ❌ “只要发生 channel 死锁,runtime 一定立即报错;加大 buffer 就能解决。”
- ✅ “runtime 主要能发现进程级无进展;局部等待环可能只是 hang,必须靠协议、指标和 profile 排查。”
自测追问:为什么系统仍有 timer 或其他 runnable goroutine 时,局部死锁可能不触发 fatal?多 producer 由谁 close?
10 分钟版(原理 + 图示)¶
经典模式
flowchart LR
A[main send] -->|无缓冲| B[无 recv G]
B -.->|永久 park| A
| 场景 | 现象 |
|---|---|
| 单 G 无缓冲自收发 | 立即 fatal deadlock |
| 缓冲满且无消费者 | send 阻塞,若仅相关 G 全阻塞 → fatal |
| 只 send 不 close,range 永远等 | 消费者挂起(若还有其他 runnable G 不 fatal) |
| 环形等待 A→B→C→A | 相关 goroutine 永久挂起;若系统仍有其他活动,不一定触发 runtime 全局死锁 |
runtime 检测边界:checkdead 主要从是否还有 running M/G 出发,并继续考虑 timer、
netpoll、cgo callback、c-shared/c-archive 等特殊路径。它不是 channel 专用的等待图检测器,
也不能证明业务局部一定有进展。
与 mutex 死锁区别:channel 无死锁检测器;mutex 循环等待同样可能只表现为 hang。
生产场景¶
- 启动期:
ch <- initDone在 main,worker 未启动 → 启动卡死。 - 批处理:
wg.Wait()在消费者,生产者等 wg → 经典死锁环。 - HTTP handler 内同步等 channel,上游超时断开,下游永远等。
排查与工具¶
SIGQUIT/kill -QUIT→ 打印所有 G 栈,搜chan receive/chan sendcurl :6060/debug/pprof/goroutine?debug=2go tool trace看无进展时间段- 单元测试加
-timeout
架构取舍¶
- 同步改异步:用 errgroup + context 替代双向阻塞握手。
- 超时必备:优先使用
context;也可用time.After/NewTimer。Go 1.23+ 可回收不再被引用的未触发 timer,旧语言版本或高频循环仍应注意 timer 分配,并在需要复用/停止时用NewTimer。 - 不宜:用无缓冲 chan 做「函数返回值」替代 —— 用 future 模式也要防无人读。
深挖问答¶
- 两个 G 互相无缓冲 send? → 两者会形成局部死锁;只有整个 Go 程序也没有其他 可运行或可唤醒路径时,runtime 才会报全局 deadlock。
- 有缓冲 size 1 会死锁吗? → 可能,若双方都先 send 满或后 recv 空。
- close 能解开吗? → recv 得零值;blocked send 仍可能 panic 若已 close。
- select 能防死锁吗? → 只有可执行的取消/超时/default 分支才能避免该次永久阻塞;
default还可能制造 busy loop,且不能自动消除系统中的循环等待。 - 和 sync.Mutex 组合? → 持锁等 chan 常见死锁,锁顺序要一致。
反模式与事故¶
go func(){ ch <- result }()但主流程已 return,无人 recv(泄漏非 always fatal)。- 在
init或单测里无缓冲握手忘记起 goroutine。 - 用 channel 实现信号量却 size=0 且无配对。
代码示例¶
// 死锁:main 是唯一 G
func bad() {
ch := make(chan int)
ch <- 1 // fatal: all goroutines are asleep
}
// 修复:异步消费者或缓冲
func good() {
ch := make(chan int, 1)
go func() { fmt.Println(<-ch) }()
ch <- 1
}