Golang并发编程如何避免死锁_Go语言死锁问题分析

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.WaitGroupcontext 协调关闭时机
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.WaitGroupWait() 会阻塞直到计数归零。若 Add() 调用晚于 Go 启动,或漏掉 Done(),main 就永远卡住 —— 表现类似死锁,但实际是逻辑遗漏。

  • 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 计数是否真正反映并发意图。