Go中常见死锁是向无缓冲channel发送或接收时无人配对,导致所有goroutine阻塞并panic;无缓冲channel要求发送与接收严格并发配对,同线程顺序执行必死锁。

channel 操作不匹配导致死锁
Go 中最常见死锁场景是 goroutine 向无缓冲 channel 发送数据,但没有其他 goroutine 在同一时刻接收;或从空 channel 接收,却无人发送。运行时检测到所有 goroutine 都阻塞且无法唤醒,就会 panic:fatal error: all goroutines are asleep - deadlock!
- 无缓冲 channel 必须「一发一收」严格配对,发送和接收必须并发发生
- 使用
select+default可避免永久阻塞,但不解决逻辑错配问题 - 若 sender 和 receiver 在同一线程(如 main)顺序执行,必然死锁:例如先
c 再,无 goroutine 并发参与
func main() {
c := make(chan int)
c <- 1 // 死锁:没人接收
fmt.Println(<-c)
}range 遍历未关闭的 channel
range 会持续等待 channel 关闭才退出。如果 sender 忘记调用 close(c),或关闭过早(还有 goroutine 试图发送),都会引发死锁或 panic。
- 仅当所有 sender 都完成且明确不再发送时,才由最后一个 sender 或协调者调用
close() - receiver 不应 close channel —— Go 约定:sender 负责关闭
- 多个 sender 时,用
sync.WaitGroup或context协调关闭时机
func main() {
c := make(chan int)
go func() {
c <- 1
c <- 2
close(c) // 必须关闭,否则 range 永不结束
}()
for v := range c {
fmt.Println(v)
}
}mutex 重复加锁或锁顺序不一致
sync.Mutex 是不可重入锁。同一个 goroutine 连续两次 Lock() 会永远阻塞;多个 mutex 交叉加锁(如 goroutine A 先锁 m1 再锁 m2,goroutine B 反之)则极易形成循环等待。
- 不要在持有锁期间调用可能再次获取同一锁的函数(包括方法内嵌调用)
- 多锁场景下,始终按固定全局顺序加锁(如按 struct 字段地址排序,或约定
m1总在m2前) - 考虑用
sync.RWMutex替代,读多写少时可降低争用
WaitGroup 使用不当阻塞主 goroutine
sync.WaitGroup 的 Wait() 会阻塞直到计数归零。若 Add() 调用晚于 Go 启动,或漏掉 Done(),main 就永远卡住 —— 表现类似死锁,但实际是逻辑遗漏。
立即学习“go语言免费学习笔记(深入)”;
-
Add()必须在go语句前调用(或至少在 goroutine 启动前完成),否则计数可能为 0 就进入Wait() - 每个 goroutine 必须确保执行一次
Done(),哪怕提前 return 或 panic,推荐用defer wg.Done() - 不要在循环中反复
Wait(),除非每次Add()/Done()都成对出现
func main() {
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1) // 必须放 goroutine 外面
go func() {
defer wg.Done() // 确保执行
time.Sleep(time.Second)
}()
}
wg.Wait() // 等所有完成
}死锁往往不是单点错误,而是多个同步原语在时序和职责上的隐含耦合。调试时别只盯 panic 位置,重点检查 channel 生命周期、锁作用域边界、以及 WaitGroup 计数是否真正反映并发意图。











