Go语言共享内存并发需避免裸用counter++,应优先用sync.Mutex保护临界区或sync/atomic实现无锁原子操作;channel适用于通信场景,不替代锁或原子操作保护共享状态。

Go 语言支持共享内存并发,但不推荐在业务逻辑层直接裸用 counter++ 这类非原子操作——它几乎必然引发数据竞争,且 go run -race 会立刻报出 Data race on variable counter。
sync.Mutex 是共享内存同步最常用、最直观的起点
当你必须让多个 goroutine 修改同一变量(比如计数器、状态标记、缓存 map),sync.Mutex 提供了最易理解的“串行化”保护。它的核心不是“锁住变量”,而是“锁住一段临界区代码”。
- 必须成对使用
mu.Lock()和mu.Unlock(),且解锁前不能 panic;建议用defer mu.Unlock()避免遗漏 - 锁对象要被所有 goroutine 共享,通常声明为包级变量或通过指针传入,不能每次新建一个
sync.Mutex{} - 锁粒度越小越好:只包裹真正需要互斥的读写操作,避免把耗时 I/O 或计算逻辑也包进去
- 注意死锁风险:同一个 goroutine 不可重复调用
Lock()(sync.RWMutex的RLock()可重入,但Lock()不行)
sync.Atomic 提供无锁、高性能的整数/指针原子操作
当共享数据只是基础类型(int32、int64、uint32、uintptr、*T),且操作足够简单(加减、比较并交换、载入、存储),sync.Atomic 比 sync.Mutex 更轻量、无锁、性能更高。
- 常见误用:
atomic.AddInt64(&counter, 1)正确;counter++即使加了atomic.LoadInt64读取也不行——这不是原子操作 -
atomic.Value可安全读写任意类型(如map[string]int),但写入必须是整体替换,不能修改内部字段 - 不支持复合操作(比如“如果值为 X 则设为 Y,否则不做”需用
CompareAndSwap循环实现)
sync.WaitGroup 用于等待 goroutine 完成,不是保护共享数据的工具
sync.WaitGroup 常被误认为“能解决竞争”,但它只负责计数和阻塞,本身不提供任何内存同步语义。如果你用它等 goroutine 结束后才读 counter,仍可能读到未刷新的旧值(尤其在没有其他同步手段时)。
立即学习“go语言免费学习笔记(深入)”;
- 它必须和真正的同步机制(
Mutex、Atomic、channel)配合使用 -
Add()必须在 goroutine 启动前调用,不能在 goroutine 内部调用(否则有竞态) - 它不保证 goroutine 内部对共享变量的写操作对主 goroutine 可见——这依赖于锁或原子操作带来的 happens-before 关系
为什么 channel 不适合替代所有共享内存场景
虽然 Go 推崇 “通过通信共享内存”,但 channel 并非万能。当你需要高频、低延迟地更新一个状态(比如每毫秒更新一次指标),或需要随机读写某个结构体字段时,用 channel 中转会引入额外拷贝、调度开销和逻辑绕弯。
- 频繁发送小数据到 channel(如单个
int)比atomic.StoreInt64慢一个数量级以上 - 无法像
map那样做 O(1) 查找;若用 channel 实现类似功能,需额外维护映射关系,复杂度陡增 - channel 是线性通信模型,不适合表达“多个 goroutine 同时读写同一份配置”的语义
真正容易被忽略的是:共享内存同步的有效性,不取决于你用了什么原语,而取决于是否建立了正确的 happens-before 关系。哪怕用了 sync.Mutex,如果读操作没在锁内进行,或者用了不同锁实例,照样竞争。验证它最简单的方式,永远是 go run -race + 真实压测。


















