go语言并发中,mutex用于写多场景的临界区保护,rwmutex适用于读多写少的高效读写协调,waitgroup则用于等待goroutine任务完成;三者需组合使用并遵循小粒度锁、清晰等待等原则。

Go 语言的并发模型强调“不要通过共享内存来通信,而要通过通信来共享内存”,但实际开发中,仍常需安全地访问共享资源。这时,Mutex、RWMutex 和 WaitGroup 这三类同步原语就成为关键工具——它们不负责协程调度,而是解决数据竞争、读写协调与协作等待问题。
Mutex:保护临界区,避免竞态
sync.Mutex 是最基础的互斥锁,用于确保同一时间只有一个 goroutine 能进入临界区。它不区分读写,适合写多或读写频率接近的场景。
- 使用前无需初始化,零值即为有效未锁定状态
- 必须成对调用
Lock()和Unlock();推荐用defer mu.Unlock()防止遗漏 - 不可重入:同一个 goroutine 重复
Lock()会导致死锁 - 锁的作用对象是变量(如结构体字段),不是数据本身;要确保所有访问共享字段的地方都加锁
例如,安全地累加计数器:
立即学习“go语言免费学习笔记(深入)”;
var mu sync.Mutex
var count int
func increment() {
mu.Lock()
defer mu.Unlock()
count++
}RWMutex:读多写少时提升并发吞吐
sync.RWMutex 提供读锁(RLock/RUnlock)和写锁(Lock/Unlock)。多个 goroutine 可同时持有读锁,但写锁是独占的,且会阻塞新读锁的获取(直到当前读锁全部释放)。
- 适用于读操作远多于写操作的场景(如配置缓存、状态快照)
- 写锁优先级更高:一旦有 goroutine 请求写锁,后续的读锁会被阻塞,防止写饥饿
- 注意:读锁不能升级为写锁;若需先读后写,应先释放读锁再申请写锁(并重新校验条件)
- 同样建议用
defer确保解锁,尤其在读锁路径中容易忽略
典型用法:
立即学习“go语言免费学习笔记(深入)”;
var rwmu sync.RWMutex
var data map[string]int
func read(key string) (int, bool) {
rwmu.RLock()
defer rwmu.RUnlock()
v, ok := data[key]
return v, ok
}
func write(key string, val int) {
rwmu.Lock()
defer rwmu.Unlock()
data[key] = val
}WaitGroup:协调 goroutine 生命周期,等待任务完成
sync.WaitGroup 不是锁,而是用于“等待一组 goroutine 结束”的计数信号量。它通过 Add、Done(或 Delta)和 Wait 三步协同工作。
-
WaitGroup必须在启动 goroutine 前调用Add(n);传入负数会 panic -
Done()等价于Add(-1),应在每个 goroutine 结束时调用(通常配合defer) -
Wait()会阻塞,直到计数归零;可在主线程中调用,也可在其他 goroutine 中等待 - 不能复制已使用的
WaitGroup变量(零值可安全拷贝,但非零值拷贝后行为未定义) - 不适合传递给 goroutine 做“子任务等待”——应传指针,或确保生命周期可控
常见模式:
立即学习“go语言免费学习笔记(深入)”;
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
fetch(u) // 执行耗时操作
}(url)
}
wg.Wait() // 主线程等待全部完成组合使用:真实场景中的典型搭配
单一原语往往不够。比如构建一个带缓存的 HTTP 客户端:
- 用
RWMutex保护缓存 map 的读写(读频繁,写仅在首次加载或失效时) - 用
Once或双重检查 +Mutex避免缓存击穿(多个 goroutine 同时发现缓存为空,只允许一个去加载) - 用
WaitGroup等待一批异步预热请求完成,再启动服务
关键原则:锁粒度尽量小,等待逻辑尽量清晰,避免锁内做网络 I/O 或长时间计算;优先考虑 channel 通信替代共享内存,再辅以同步原语兜底。










