sync包需在真实竞态场景中实践掌握,核心是判断读写主体、频率及阻塞容忍度;mutex用于多字段/结构体/非幂等操作,原子操作仅适用于单变量;rwmutex读锁不互斥但受写锁阻塞;waitgroup的add必须在go前调用;once确保初始化只执行一次但不重试失败。

sync 包不是靠“语言学习”掌握的,而是靠在真实竞态场景中识别问题、选对原语、并验证行为是否符合预期。它不考验语法记忆,而考验你对“谁在读、谁在写、频率如何、能否容忍阻塞”这些条件的即时判断。
什么时候必须用 sync.Mutex 而不是原子操作?
当你需要保护的不是单个整数或指针,而是多个字段、结构体状态变更、或带副作用的操作(比如先查后改、非幂等日志)时,atomic 就不够用了。
常见错误现象:fatal error: concurrent map writes 或计数器最终值远小于预期——这说明你试图用 map 或复合逻辑做并发访问,却只加了 atomic.AddInt64。
-
sync.Mutex适合:临界区包含多行逻辑、需保证顺序性(如“检查是否存在→插入”)、或涉及非原子类型(string、struct、map) - 别把
Lock()放在循环里,也别在锁内调用可能阻塞的函数(如http.Get),否则会拖垮并发吞吐 - 用
go run -race运行程序,它能直接标出哪一行触发了竞态,比猜快得多
sync.RWMutex 的读锁真的不阻塞其他读协程吗?
是的,但前提是没人在持写锁。一旦有 goroutine 调用了 Lock(),后续所有 RLock() 都会排队等待——哪怕写锁还没真正进入临界区。
使用场景:缓存数据(如配置、白名单)、统计指标(如请求计数+响应时间分布),读频次远高于写(比如 1000:1)。
- 不要在读锁内调用可能升级为写的函数(比如
getOrCreate),否则容易死锁 -
RUnlock()必须和RLock()成对出现;漏掉一个会导致后续所有写操作永久阻塞 - 如果读操作本身很重(比如深拷贝大结构体),RWMutex 不会帮你优化性能,反而因锁竞争变慢
sync.WaitGroup 的 Add() 为什么必须在 go 之前调用?
因为 WaitGroup 的计数器是无锁原子更新的,但 Add() 和 Done() 的调用顺序决定了主 goroutine 是否能正确感知子 goroutine 启动完成。如果在 go 内部才 Add(1),主 goroutine 可能在任何子 goroutine 执行前就调用 Wait() 并立即返回——此时计数器还是 0。
- 典型错误写法:
go func() { wg.Add(1); ...; wg.Done() }()→wg.Wait()永远不会等 - 安全写法:要么在
go前调用wg.Add(1),要么提前wg.Add(n)(n 是确定的 goroutine 数量) -
WaitGroup不能被复制,传参时务必传指针*sync.WaitGroup
sync.Once 的 Do() 真的只执行一次,但要注意什么?
是的,底层靠一个 uint32 状态位 + 原子 CAS 实现,即使 1000 个 goroutine 同时调用 once.Do(f),也仅有一个 f 会执行,其余全部等待并直接返回。
但它不解决初始化失败重试的问题——如果 f() panic 或返回错误,Once 仍标记为“已完成”,后续调用不再尝试。
- 别在
f中做不可逆操作(如打开文件但不检查 err),否则失败后整个程序无法恢复 - 如果初始化逻辑依赖外部状态(如环境变量、网络配置),建议包装一层重试或 fallback 逻辑,别全交给
Once -
sync.Once不提供获取是否已执行的方法,也没有 Reset 接口,设计上就是“一锤定音”
map 并发写就本能加 Mutex,看到读多写少就换 RWMutex,但实际中更常见的是——该用 sync.Map 的地方硬套 RWMutex + map,或该用 atomic.Value 存配置快照的地方,却反复锁读取。判断依据永远来自压测数据和 -race 输出,而不是直觉。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











