sync.Mutex 适合保护共享变量,需在访问前 Lock、结束后 Unlock;避免返回带锁字段指针;sync.Once 保证初始化仅一次且不可重置;sync.WaitGroup 要先 Add 再 go,用 defer Done;sync.RWMutex 适用于读多写少场景。

sync.Mutex 适合保护共享变量,但别在返回值里传指针
多个 goroutine 同时读写一个 int、map 或结构体字段时,不加锁大概率触发 fatal error: concurrent map writes 或读到脏数据。用 sync.Mutex 最直接:在访问前调用 mu.Lock(),结束后立刻 mu.Unlock()。
常见错误是把带锁的结构体字段以指针形式返回,比如:
func (c *Counter) GetPtr() *int {
c.mu.Lock()
return &c.val // 危险!外部可绕过锁直接改
}
应该改为只返回值,或提供受控的修改方法:
- 返回副本:
return c.val - 封装操作:
func (c *Counter) Inc() { c.mu.Lock(); defer c.mu.Unlock(); c.val++ } - 避免暴露内部状态,尤其别让调用方决定何时解锁
sync.Once 保证初始化只执行一次,但不能重置
sync.Once 的 Do() 方法适合做单例初始化、配置加载、资源首次创建等场景。它内部用原子操作 + 互斥锁实现,线程安全且开销极小。
立即学习“go语言免费学习笔记(深入)”;
注意它不可重置、不可重复使用。一旦 once.Do(f) 执行过,再调用同个 once 实例的 Do() 将直接返回,不管 f 是否 panic(panic 会导致该次调用失败,但标记仍设为已执行)。
- 不要试图通过新建
sync.Once变量来“重试”初始化 - 如果初始化逻辑可能失败且需重试,应自己封装重试逻辑,而不是依赖
sync.Once - 典型误用:
if err := initDB(); err != nil { once.Do(initDB) }—— 这不会重试,因为once已标记完成
sync.WaitGroup 用于等待 goroutine 结束,别漏掉 Add 或 Done
sync.WaitGroup 是等待一组 goroutine 完成的最常用方式。核心是三步:wg.Add(n)、启动 goroutine、每个 goroutine 结尾调用 wg.Done(),主 goroutine 调用 wg.Wait() 阻塞等待。
最容易出错的是调用时机:Add() 必须在 go 语句之前,否则可能 Wait() 已返回而 goroutine 还没被计数;Done() 必须确保执行,哪怕函数 panic,建议用 defer wg.Done()。
- 错误写法:
go func() { wg.Add(1); ...; wg.Done() }()——Add在 goroutine 内,竞态风险高 - 正确写法:
wg.Add(1); go func() { defer wg.Done(); ... }() - 如果 goroutine 数量动态,先算好数量再
Add,别靠len(ch)之类估算
sync.RWMutex 读多写少时提升性能,但写锁会阻塞所有读
当共享数据读操作远多于写操作(比如配置缓存、路由表),sync.RWMutex 比普通 Mutex 更高效:多个 goroutine 可同时持有读锁,但写锁会独占,且会阻塞新读锁请求(已有读锁不强制释放)。
关键点在于锁粒度和使用习惯:读锁用 RLock()/RUnlock(),写锁仍用 Lock()/Unlock()。混用时注意——写锁必须等所有当前读锁释放后才能获取。
- 别在持有读锁时调用可能升级为写锁的函数(Go 不支持锁升级)
- 写操作频繁时,
RWMutex可能比Mutex更慢,因读锁维护有额外开销 - 用
go tool trace观察sync相关阻塞事件,确认是否真因读锁堆积导致延迟










