Go 运行时检测到所有 goroutine 都在等待且无法唤醒时会 panic 并终止程序;预防需从 channel 生命周期、goroutine 协作逻辑和同步原语使用三方面入手,无缓冲 channel 发送前必须有并发接收者。
go 运行时一旦发现所有 goroutine 都在阻塞且无法被唤醒,会立即 panic 并输出 fatal error: all goroutines are asleep - deadlock!。这不是 recoverable 错误,程序直接终止。活锁虽不 panic,但 cpu 空转、任务无进展,本质是资源调度失效——两者都源于对 channel、锁、waitgroup 的协作逻辑理解偏差或使用失当。
无缓冲 channel 发送前必须有并发接收者
这是最常见、最易复现的死锁来源。无缓冲 channel 的 c 是同步操作:发送方会一直阻塞,直到另一端有 goroutine 执行 <code>。
典型错误写法:
ch := make(chan int)
ch <- 42 // 主 goroutine 卡在这里,无人接收
fmt.Println("never reached")
正确做法是确保接收者与发送者并发运行:
- 先启动接收 goroutine,再执行发送
- 或改用带缓冲 channel(如
make(chan int, 1)),但仅缓解,不根治——缓冲满后仍会阻塞 - 避免在单个 goroutine 内顺序做
c 和 <code>,这等价于自锁
sync.Mutex 不支持重入且严禁交叉加锁
sync.Mutex 是非重入锁。同一个 goroutine 连续调用两次 mu.Lock()(中间没 mu.Unlock()),或两个 goroutine 分别以相反顺序获取两把锁(A→B vs B→A),都会触发死锁 panic。
安全实践包括:
- 每个
mu.Lock()后紧跟defer mu.Unlock(),不跨分支手动解锁 - 多锁场景下,始终按固定顺序加锁(例如按变量名、内存地址或 ID 大小排序)
- 避免在持有锁期间调用可能间接再次请求同一把锁的函数(包括方法)
WaitGroup 计数必须严格匹配且 Add 在 goroutine 启动前完成
wg.Wait() 会永久阻塞,直到内部计数归零。漏调 wg.Add(n)、wg.Done() 被 panic 跳过、或 Done() 放在 goroutine 末尾而非入口 defer,都会导致卡死。
关键约束:
-
wg.Add(n)必须在go语句之前调用,不能在 goroutine 内动态 Add - 每个 goroutine 入口第一行应为
defer wg.Done(),确保无论是否 panic 都能计数减一 - 不要复制
sync.WaitGroup值,传递指针(&wg)
活锁常出现在无超时的 channel 争抢或分布式锁轮询中
活锁没有 panic,但 goroutine 持续运行却无法推进业务。典型场景是多个 goroutine 反复尝试获取同一资源后立即放弃,又马上重试,形成“让来让去”的空转循环。
比如用 select 轮询 channel 但没设 default 或超时:
for {
select {
case v := <-ch:
process(v)
}
}
若 ch 永远不发数据,该 goroutine 就活锁。更隐蔽的是分布式锁轮询——客户端不断重试 SETNX,失败就 sleep 再试,不加指数退避和最大重试限制,就会耗尽 CPU 却拿不到锁。
应对方式:
- channel 操作务必配
select+time.After或context.WithTimeout - 分布式锁客户端必须实现自动过期、续期机制和退避策略
- 避免纯忙等待;优先用 channel 通知替代轮询
真正难防的不是单点错误,而是多个同步原语交织时的隐式依赖——比如一个 goroutine 持有 mutex 的同时向 channel 发送,而接收方又试图获取同一把 mutex。这种链式阻塞很难靠静态检查发现,只能靠设计阶段明确资源获取顺序,并在测试中覆盖边界路径。


















