2.6. 并发陷阱

2.6.1. 概述

Go 的并发模型虽然简洁,但仍然有许多容易踩坑的地方。本节总结了最常见的并发陷阱及其解决方案。

2.6.2. 陷阱 1:数据竞争 (Data Race)

2.6.2.1. 问题描述

当多个 goroutine 同时访问同一个变量,且至少有一个是写操作时,就会发生数据竞争。

// ❌ 数据竞争示例
var counter int

func main() {
    for i := 0; i < 1000; i++ {
        go func() {
            counter++ // 数据竞争!
        }()
    }
    time.Sleep(time.Second)
    fmt.Println(counter) // 结果不确定
}

2.6.2.2. 检测方法

go run -race main.go
go test -race ./...

2.6.2.3. 解决方案

// ✅ 方案 1:使用 Mutex
var (
    counter int
    mu      sync.Mutex
)

func increment() {
    mu.Lock()
    counter++
    mu.Unlock()
}

// ✅ 方案 2:使用 atomic
var counter int64

func increment() {
    atomic.AddInt64(&counter, 1)
}

// ✅ 方案 3:使用 channel
func main() {
    counter := make(chan int, 1)
    counter <- 0
    
    for i := 0; i < 1000; i++ {
        go func() {
            v := <-counter
            v++
            counter <- v
        }()
    }
}

2.6.3. 陷阱 2:闭包捕获循环变量

2.6.3.1. 问题描述

// ❌ 所有 goroutine 都打印相同的值
func main() {
    for i := 0; i < 5; i++ {
        go func() {
            fmt.Println(i) // 可能全部打印 5
        }()
    }
    time.Sleep(time.Second)
}

2.6.3.2. 解决方案

// ✅ 方案 1:传递参数
for i := 0; i < 5; i++ {
    go func(n int) {
        fmt.Println(n)
    }(i)
}

// ✅ 方案 2:创建局部变量
for i := 0; i < 5; i++ {
    i := i // 创建新变量
    go func() {
        fmt.Println(i)
    }()
}

// ✅ Go 1.22+ 自动修复(循环变量每次迭代都是新的)

2.6.4. 陷阱 3:Goroutine 泄漏

2.6.4.1. 问题描述

// ❌ goroutine 永远阻塞
func leakyFunction() {
    ch := make(chan int)
    go func() {
        val := <-ch // 永远阻塞,因为没有发送者
        fmt.Println(val)
    }()
    // 函数返回,但 goroutine 仍然存在
}

2.6.4.2. 解决方案

// ✅ 使用 context 控制生命周期
func nonLeakyFunction(ctx context.Context) {
    ch := make(chan int)
    go func() {
        select {
        case val := <-ch:
            fmt.Println(val)
        case <-ctx.Done():
            return // 优雅退出
        }
    }()
}

// ✅ 使用 buffered channel
func nonLeakyFunction2() {
    ch := make(chan int, 1)
    go func() {
        ch <- 42 // 不会阻塞
    }()
    // 即使不读取,goroutine 也能完成
}

2.6.5. 陷阱 4:死锁

2.6.5.1. 常见死锁场景

2.6.5.1.1. 场景 1:channel 自己等自己

// ❌ 死锁
func main() {
    ch := make(chan int)
    ch <- 1  // 阻塞,等待接收者
    <-ch     // 永远执行不到
}

// ✅ 修复
func main() {
    ch := make(chan int)
    go func() {
        ch <- 1
    }()
    <-ch
}

2.6.5.1.2. 场景 2:循环等待

// ❌ 两个 goroutine 互相等待
var mu1, mu2 sync.Mutex

// goroutine 1
go func() {
    mu1.Lock()
    time.Sleep(time.Millisecond)
    mu2.Lock() // 等待 goroutine 2 释放
    mu2.Unlock()
    mu1.Unlock()
}()

// goroutine 2
go func() {
    mu2.Lock()
    time.Sleep(time.Millisecond)
    mu1.Lock() // 等待 goroutine 1 释放
    mu1.Unlock()
    mu2.Unlock()
}()

// ✅ 修复:按固定顺序获取锁

2.6.5.1.3. 场景 3:WaitGroup 使用错误

// ❌ 死锁:Wait 在 Add 之前
var wg sync.WaitGroup

go func() {
    wg.Add(1)
    defer wg.Done()
    // ...
}()

wg.Wait() // 可能在 Add 之前执行

// ✅ 修复:Add 在启动 goroutine 之前
var wg sync.WaitGroup
wg.Add(1)

go func() {
    defer wg.Done()
    // ...
}()

wg.Wait()

2.6.6. 陷阱 5:不正确的锁粒度

2.6.6.1. 锁粒度太大

// ❌ 锁住整个操作,包括 I/O
func (s *Server) HandleRequest(req Request) Response {
    s.mu.Lock()
    defer s.mu.Unlock()
    
    // 网络请求被锁保护,导致所有请求串行
    result := s.callExternalService(req)
    return result
}

// ✅ 只锁住需要保护的部分
func (s *Server) HandleRequest(req Request) Response {
    // 网络请求不需要锁
    result := s.callExternalService(req)
    
    s.mu.Lock()
    s.cache[req.ID] = result
    s.mu.Unlock()
    
    return result
}

2.6.6.2. 锁粒度太小

// ❌ 多次加锁解锁,开销大
func (c *Counter) AddAndGet() int {
    c.mu.Lock()
    c.count++
    c.mu.Unlock()
    
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count // 可能已经被其他 goroutine 修改
}

// ✅ 原子操作
func (c *Counter) AddAndGet() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
    return c.count
}

2.6.7. 陷阱 6:错误的 channel 关闭

// ❌ 多次关闭 channel
func bad() {
    ch := make(chan int)
    close(ch)
    close(ch) // panic!
}

// ❌ 向已关闭的 channel 发送
func bad2() {
    ch := make(chan int)
    close(ch)
    ch <- 1 // panic!
}

// ✅ 使用 sync.Once 确保只关闭一次
type SafeChannel struct {
    ch   chan int
    once sync.Once
}

func (sc *SafeChannel) Close() {
    sc.once.Do(func() {
        close(sc.ch)
    })
}

2.6.8. 陷阱 7:select 中的优先级问题

// ❌ 可能永远不会处理 done
func process(done chan struct{}, data chan int) {
    for {
        select {
        case <-done:
            return
        case v := <-data:
            process(v)
        }
    }
}

// 如果 data 一直有数据,done 可能永远不被选中

// ✅ 每次循环都检查 done
func process(done chan struct{}, data chan int) {
    for {
        select {
        case <-done:
            return
        default:
        }
        
        select {
        case <-done:
            return
        case v := <-data:
            process(v)
        }
    }
}

2.6.9. 陷阱 8:time.After 在循环中使用

// ❌ 每次循环都创建新的 timer,内存泄漏
for {
    select {
    case <-ch:
        // ...
    case <-time.After(time.Second): // 每次都分配新的 timer
        // ...
    }
}

// ✅ 复用 timer
timer := time.NewTimer(time.Second)
defer timer.Stop()

for {
    select {
    case <-ch:
        if !timer.Stop() {
            <-timer.C
        }
        timer.Reset(time.Second)
    case <-timer.C:
        timer.Reset(time.Second)
    }
}

2.6.10. 陷阱 9:map 并发读写

// ❌ panic: concurrent map read and map write
var m = make(map[string]int)

go func() {
    for {
        m["key"] = 1
    }
}()

go func() {
    for {
        _ = m["key"]
    }
}()

// ✅ 使用 sync.RWMutex
var (
    m  = make(map[string]int)
    mu sync.RWMutex
)

func set(k string, v int) {
    mu.Lock()
    m[k] = v
    mu.Unlock()
}

func get(k string) int {
    mu.RLock()
    defer mu.RUnlock()
    return m[k]
}

// ✅ 或使用 sync.Map
var m sync.Map

m.Store("key", 1)
v, _ := m.Load("key")

2.6.11. 陷阱 10:忽略 context 取消

// ❌ 忽略 context,无法取消
func longOperation(ctx context.Context) error {
    for i := 0; i < 1000000; i++ {
        heavyComputation()
    }
    return nil
}

// ✅ 定期检查 context
func longOperation(ctx context.Context) error {
    for i := 0; i < 1000000; i++ {
        if ctx.Err() != nil {
            return ctx.Err()
        }
        heavyComputation()
    }
    return nil
}

// ✅ 在长操作中使用 select
func longOperation(ctx context.Context) error {
    for i := 0; i < 1000000; i++ {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
            heavyComputation()
        }
    }
    return nil
}

2.6.12. 总结

陷阱

检测方法

解决方案

数据竞争

-race 标志

mutex/atomic/channel

闭包捕获

代码审查

传参/局部变量

Goroutine 泄漏

监控 NumGoroutine

context/buffered channel

死锁

-race/死锁检测器

固定锁顺序/超时

锁粒度问题

性能分析

合理划分临界区

channel 关闭

代码审查

sync.Once

select 优先级

代码审查

双重 select

time.After 泄漏

内存分析

复用 timer

map 并发

-race 标志

RWMutex/sync.Map

忽略 context

代码审查

定期检查 ctx.Done()

2.6.13. 参考资源