在Go语言并发程序中,当多个 goroutine 同时读写同一共享变量时,如果不加以控制,会引发数据竞争(race condition),导致程序行为不可预期。
Go 提供了多种同步原语,最基本的就是 sync.Mutex 和 sync.RWMutex。
一、什么是数据竞争?
数据竞争发生在两个或多个 goroutine 同时访问相同的内存地址,并且至少有一个是写操作,且未使用同步机制。
表现包括:
- • 输出错乱、值错误
- • 程序崩溃或逻辑失常
- • 非确定性 bug(最难排查)
二、sync.Mutex:互斥锁
sync.Mutex 是最常见的锁,用于保护临界区,使得同一时间只有一个 goroutine 可以进入。
示例:未加锁的并发写入(错误)
var counter int func increment() { for i := 0; i < 1000; i++ { counter++ } } func main() { for i := 0; i < 10; i++ { go increment() } time.Sleep(time.Second) fmt.Println("Counter:", counter) // 不确定输出 }
正确使用 Mutex 加锁
var ( counter int mutex sync.Mutex ) func increment() { for i := 0; i < 1000; i++ { mutex.Lock() counter++ mutex.Unlock() } }
三、sync.RWMutex:读写互斥锁
相比 Mutex,sync.RWMutex 提供了更细粒度的控制:
| 操作 | 行为说明 |
Lock() |
获取写锁,所有读写都被阻塞 |
RLock() |
获取读锁,允许多个同时读 |
| 写锁优先级高 | 有写者等待时,新的读锁会被阻止 |
示例:多个读协程同时访问共享资源
type SafeMap struct { data map[string]string mu sync.RWMutex } func (s *SafeMap) Get(key string) string { s.mu.RLock() defer s.mu.RUnlock() return s.data[key] } func (s *SafeMap) Set(key, val string) { s.mu.Lock() defer s.mu.Unlock() s.data[key] = val }
四、使用场景对比
| 场景 | 推荐使用 |
| 多个读协程 + 偶尔写 | RWMutex |
| 频繁读写,互斥访问临界区 | Mutex |
| 极端并发 + 只读数据 | 无需加锁(只读) |
| 精确锁粒度对性能有重要影响的系统 | RWMutex 可调优 |
五、避免死锁的建议
- • 加锁后一定记得解锁,推荐使用
defer - • 避免多个锁嵌套加锁(容易形成死锁环)
- • 尽量缩小加锁范围,不要锁整个函数
- • 避免在持锁状态下调用外部函数(可能阻塞)
六、使用 -race 检查数据竞争
Go 提供了内置的竞态检测工具:
go run -race main.go
可自动分析是否存在数据竞争,是并发调试的利器。
七、小结
- •
sync.Mutex是最基础的并发控制工具,适用于串行化访问。 - •
sync.RWMutex适用于读多写少的场景,提升并发效率。 - • 使用锁时一定要小心死锁与性能瓶颈,配合
-race工具排查隐患。