死锁必然发生而非可能出问题,报错时程序已卡死无法恢复;规避关键是在编码阶段切断循环等待路径,无缓冲 channel 发送前必须确保有接收者。

死锁不是“可能出问题”,而是只要条件凑齐就必然发生——fatal error: all goroutines are asleep - deadlock! 报错出现时,程序已经卡死,没有重试或恢复机会。规避的关键不是等它发生再修,而是在写代码时切断所有循环等待路径。
无缓冲 channel 发送前必须确认有接收者
这是最常踩的坑:main 里 ch ,没起 goroutine 接收,运行即 panic。Go 不会帮你调度“稍后有人来读”,它只认当前时刻是否就绪。
- 永远不要在单个 goroutine(尤其是 main)里做无缓冲 channel 的发送,除非你明确启动了另一个 goroutine 等着接收
- 用
select+default做防御性发送:select { case ch - 如果必须同步通信,优先用带缓冲 channel(如
make(chan int, 1)),但注意:缓冲只是延缓死锁,不是根治——最终仍需消费端取走数据
多个 sync.Mutex 一起用时,加锁顺序必须全局一致
两个 goroutine 分别按不同顺序调 muA.Lock() 和 muB.Lock(),就是 AB-BA 死锁的教科书场景。Go 的 sync.Mutex 不支持超时、不支持 tryLock,一旦卡住就永远卡住。
- 约定锁顺序:按变量声明顺序、字段名字母序(如
cacheMu→dbMu)、或业务层级(userMu→orderMu),并在注释里写死:// lock order: userMu → orderMu → paymentMu - 把多锁操作封装成函数,内部固定顺序,外部只调一次——避免把锁粒度暴露给调用方
- 别在持有
mu.Lock()期间调用其他可能也持锁的函数,除非你 100% 确认它们不碰同一组锁
sync.RWMutex 混用读写锁时,禁止在 RLock() 后调 Lock()
读锁不阻塞读锁,但会阻塞写锁;写锁则阻塞一切。常见错误是:某个函数先 mu.RLock(),中间逻辑又触发了另一个需要 mu.Lock() 的调用,结果自己把自己堵死。
- 读路径里绝对不要嵌套写操作——哪怕那个写操作看起来“只改局部变量”,也要检查是否间接触发了锁竞争
- 如果业务确实需要读中写,直接用
mu.Lock(),别试图用 RLock() + Lock() 组合,这几乎必然导致死锁 -
defer mu.RUnlock()必须紧挨着mu.RLock(),中间不能穿插任何可能 panic 的代码,否则读锁漏释放,后续所有写操作全卡住
channel 关闭必须由发送方负责,且关闭前确保无 goroutine 待发送
关闭 channel 本身不导致死锁,但关得太早或太晚都会引发连锁阻塞:关早了,发送方 panic;关晚了,接收方 for range ch 永远等不到 EOF,变成隐性死锁。
- 谁发谁关——这是铁律。接收方绝不能关 channel,也不能假设“没人发了我就关”
- 关闭前用
sync.WaitGroup或context确保所有发送 goroutine 已退出:var wg sync.WaitGroup for i := 0; i
- 接收端必须用
v, ok := 判断是否已关闭,不能只依赖 <code>for range—— 如果 channel 永不关闭,for range就永不退出
真正难防的不是报错的死锁,而是那些没 panic 却让 goroutine 永久挂起的场景:比如 select 里漏 default、WaitGroup Add/Wait 错位、context.WithCancel 被提前 cancel。这些不会立刻崩,但会让服务缓慢失能——查起来比显式死锁更费时间。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











