go sync包需按场景精准选用原语:mutex比rwmutex更轻量安全(读字段时);rwmutex不支持递归读锁且易致写饥饿;once.do不保证执行成功;waitgroup.add须在goroutine启动前调用;pool仅适用于无状态临时对象。

Go 的 sync 包不是“拿来就用”的工具箱,而是需要按场景精准选用的底层同步原语集合;误配或滥用会导致死锁、竞态、性能陡降甚至逻辑错误。
什么时候该用 sync.Mutex 而不是 sync.RWMutex
别一看到“读多写少”就默认上 RWMutex——它只在读操作明显耗时(比如深拷贝结构体、json 序列化)且并发读 goroutine 数量高时才有收益。
- 如果只是读一个
int或bool字段,sync.Mutex更轻量、更安全;RWMutex的额外状态管理反而增加调度开销 -
RWMutex不支持递归读锁:同一个 goroutine 多次调用RLock()后只调一次RUnlock()会 panic - 持续有新读请求进来时,写操作可能被无限期饿死(read starvation),尤其在高吞吐配置服务中要警惕
- 不能在持有
RLock()时尝试升级为写锁(即再调Lock()),必然死锁
sync.Once.Do 的真实行为和常见误用
sync.Once.Do 保证函数「最多执行一次」,但不保证「执行成功」——这是最常被忽略的语义细节。
- 如果传入的函数 panic,
Once会标记为“已执行”,后续调用直接返回,不会重试 - 不要把含 fallback、重试或 Error 返回处理的逻辑直接塞进
Do;应在外层封装,配合显式状态标志位 - 正确姿势是:用
initErr和inited双变量暴露初始化结果,而不是依赖Once自身状态
sync.WaitGroup 的 Add 为什么必须在 goroutine 启动前调用
这不是风格问题,而是内存可见性问题:WaitGroup.Add 和 Done 必须处于同一个 happens-before 链路中,否则 Wait() 永远阻塞。
立即学习“go语言免费学习笔记(深入)”;
-
wg.Add(1)放在go func()内部 →Done执行了,但主 goroutine 根本看不到计数器变化 →Wait()卡死 - 动态启动 goroutine 时,必须在循环外预设总数,或用闭包捕获当前值,例如:
go func(val int) { defer wg.Done(); process(val) }(item) -
WaitGroup不能被复制:结构体中嵌了sync.WaitGroup,传参必须用指针,否则副本间计数器完全隔离
sync.Pool 不是缓存,而是一把双刃剑
sync.Pool 的对象生命周期由 GC 控制,它不保证复用,也不承诺不清理——只适合「高频创建销毁 + 对象无强状态 + 可安全清空」的临时资源。
- 不要往里放含
channel、未关闭文件句柄、或带闭包引用的对象 - 对象放回池前必须手动清空敏感字段(如
userToken、password),否则可能泄露给下一个使用者 -
New函数仅在池为空时触发,不保证每次Get都调用;测试时可用GODEBUG=gctrace=1观察实际复用率
真正难的从来不是记住 API,而是判断哪个原语能建立正确的 happens-before 关系、哪个锁粒度既安全又不过度阻塞、以及哪次 panic 其实暴露了初始化逻辑本身的设计缺陷。