Go中sync.Mutex锁顺序不一致和无缓冲channel双向等待是死锁两大主因:前者需全局约定加锁顺序并避免锁内调用潜在加锁函数,后者须用select加timeout/default分支防护。

Go 中 sync.Mutex 锁顺序不一致直接导致死锁
多个 goroutine 同时获取多个锁,但加锁顺序不统一,是死锁最典型的来源。比如 A goroutine 先锁 mu1 再锁 mu2,B goroutine 却先锁 mu2 再锁 mu1,只要它们交叉执行,就卡死。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 所有需要同时持有多把锁的代码路径,必须约定全局加锁顺序(例如按变量地址、按 struct 字段名字符串排序,或简单按锁变量声明顺序)
- 避免在持有锁期间调用可能间接获取其他锁的函数(如日志库、DB 方法、回调钩子)
- 用
go tool trace或runtime.SetMutexProfileFraction(1)配合 pprof 查看锁竞争热点,但无法直接定位顺序问题——得靠代码审查 - 示例错误写法:
// goroutine A mu1.Lock() mu2.Lock() // 可能阻塞 <p>// goroutine B<br /> mu2.Lock() mu1.Lock() // 必然死锁
channel 操作在无缓冲 channel 上双向等待
无缓冲 channel 的 send 和 recv 是同步配对操作;如果两个 goroutine 分别只发不收、只收不发,且没有超时或 select 保护,就会永久阻塞,触发 fatal error: all goroutines are asleep - deadlock。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 永远不要假设对方“一定会收/发”——尤其跨包、跨模块通信时
- 用
select包裹 channel 操作,至少加上default或timeout分支,避免无限等待 - 启动 goroutine 前确认 channel 已初始化,且容量与使用模式匹配(例如通知类用
make(chan struct{}, 1)) - 示例陷阱:
c := make(chan int) go func() { c <- 42 }() // 发送方启动 <-c // 主 goroutine 等接收 → 死锁!因为没并发 recv
WaitGroup 使用中 Wait() 被提前调用或计数不匹配
sync.WaitGroup 不是锁,但误用会导致主线程在 goroutine 还没启动或还没完成时就调用 wg.Wait(),结果卡住不动——表面像死锁,实际是逻辑等待空转。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
-
wg.Add()必须在 goroutine 启动前调用,不能放在 goroutine 内部(除非你明确想延迟注册) -
wg.Done()必须和wg.Add(1)严格一一对应;用defer wg.Done()是最安全的习惯 - 避免在循环中反复
wg.Add(1)却漏掉某次Done()(比如 panic 未 recover、return 提前) - 调试技巧:临时加
fmt.Printf("Add: %d, Done: %d\n", wg.Add, wg.Done)不行——WaitGroup不暴露计数;改用atomic.Int64手动跟踪更直观
资源排序锁定:不是“锁资源”,而是“按固定序锁同一批资源”
所谓“资源排序锁定”,本质是给要加锁的对象定义唯一、稳定、可比较的顺序,强制所有代码按此顺序 acquire。它不是语言特性,是人为约定 + 工具辅助的防御策略。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 对 map key、struct 字段、数据库 row ID 等,用
sort.Strings或bytes.Compare统一排序后依次加锁 - 避免用指针地址做排序依据(GC 可能移动对象,地址变化)
- 若锁对象来自外部(如用户传入的
*sync.Mutex),无法控制其顺序,应拒绝多锁操作,改用更高层抽象(如单个大锁、shard 分桶) - 一个轻量级校验思路:在测试中随机打乱锁请求顺序,用
runtime.Gosched()注入调度扰动,高频运行看是否复现死锁
真正麻烦的不是写错一行 lock,而是锁的边界随业务演进悄悄扩散——今天只锁两个字段,明天加个缓存更新,后天连带日志上下文一起锁进去。越晚发现顺序不一致,越难重构。










