在 Go 中,并发死锁是一个常见问题,尤其是在使用 goroutine 、channel 或 mutex 时。避免死锁需要理解死锁的根源,然后采取相应策略。下面我给你系统讲解:
1️⃣ 死锁的常见原因
-
循环等待
多个 goroutine 相互等待对方持有的资源,例如:
Govar mu1, mu2 sync.Mutex go func() { mu1.Lock() time.Sleep(time.Second) mu2.Lock() // 等待 mu2 mu2.Unlock() mu1.Unlock() }() go func() { mu2.Lock() time.Sleep(time.Second) mu1.Lock() // 等待 mu1 mu1.Unlock() mu2.Unlock() }()→ 互相等待,形成死锁。
-
未关闭的 channel
-
发送者发送到没有接收者的 channel。
-
接收者等待发送者,但发送者没有启动。
例:
Goch := make(chan int) ch <- 1 // 如果没人接收,死锁 -
-
缓冲区满的 channel
-
无缓冲 channel 或缓冲已满的 channel 都可能阻塞发送者。
-
如果没有其他 goroutine 及时接收,可能死锁。
-
-
所有 goroutine 阻塞
-
主 goroutine 等待子 goroutine 完成,但子 goroutine 因等待主 goroutine 或其他资源而阻塞。
-
Go 运行时会直接报
fatal error: all goroutines are asleep - deadlock!
-
2️⃣ 避免死锁的策略
(A) 正确使用 channel
-
用缓冲 channel 避免阻塞
ch := make(chan int, 1) // 带缓冲 ch <- 1 // 不会立即阻塞 -
确保接收者存在
go func() { val := <-ch fmt.Println(val) }() ch <- 1 -
关闭 channel
-
对于只读操作,发送方可以关闭 channel,让接收方通过
for range循环退出:close(ch)
for v := range ch {
fmt.Println(v)
}
-
-
使用
select避免阻塞select { case ch <- 1: fmt.Println("sent") default: fmt.Println("channel full, skip") }
(B) 正确使用 mutex(互斥锁)
-
避免嵌套锁
-
如果必须锁多个 mutex,确保所有 goroutine 按相同顺序加锁。
mu1.Lock()
mu2.Lock()
// ...
mu2.Unlock()
mu1.Unlock()
-
-
使用
defer解锁mu.Lock() defer mu.Unlock() // 函数结束自动释放锁
(C) 使用 WaitGroup 或信号量
-
sync.WaitGroup可以避免主 goroutine 提前退出导致死锁。 -
例:
var wg sync.WaitGroup
wg.Add(2)go func() {
defer wg.Done()
fmt.Println("goroutine 1")
}()go func() {
defer wg.Done()
fmt.Println("goroutine 2")
}()wg.Wait() // 等待所有 goroutine 完成
(D) 检查所有 goroutine 的阻塞点
-
发送/接收 channel
-
mutex 锁
-
select default 分支是否被阻塞
-
WaitGroup Done() 是否漏掉
(E) 工具辅助
-
使用
go run -race检测竞态条件。 -
使用
pprof或runtime.Stack()分析死锁。 -
Go 1.18+ 有时会自动检测全局死锁并报错。
✅ 小结
避免 Go 死锁核心原则:
-
保证 所有 channel 的发送和接收成对存在。
-
加锁顺序统一,避免循环等待。
-
合理使用缓冲 channel 或
select。 -
用 WaitGroup 或 context 控制 goroutine 生命周期。
-
工具检测 +
defer解锁,减少人为错误。
