不能直接用++或=更新共享计数器,因为++是非原子的读-改-写操作,多goroutine并发时会导致数据竞争和结果错误;必须使用sync/atomic(如atomic.AddInt64)或sync.Mutex保证线程安全。

为什么不能直接用 ++ 或 = 更新共享计数器
多个 goroutine 同时对一个 int 变量执行 counter++,结果大概率小于预期值。这不是“偶尔出错”,而是根本没定义行为:++ 是读-改-写三步操作,中间可能被抢占,导致覆盖彼此的写入。Go 编译器和 CPU 都不保证其原子性,即使变量是全局或指针指向的也不行。
- 典型现象:
go run -race会报Data Race;不加-race也可能跑出随机结果 - 不是线程安全的替代方案:
sync.Mutex能解决问题,但有锁开销;而简单计数、标志位切换等场景,sync/atomic更轻量 - 必须用指针传入:
atomic系列函数操作的是内存地址,所有参数类型都要求是*T
atomic.AddInt64 和 atomic.LoadInt64 配合使用最常见
计数器增减 + 读取是原子操作最典型的组合。注意:所有整数原子操作都严格区分有符号/无符号、32/64 位,不能混用类型,否则编译失败或 panic。
var counter int64
func increment() {
atomic.AddInt64(&counter, 1)
}
func get() int64 {
return atomic.LoadInt64(&counter)
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Println("final:", get()) // 总是 100
}
-
atomic.AddInt64返回新值(可选),atomic.LoadInt64是唯一安全读取方式 - 不要用
counter++替代atomic.AddInt64(&counter, 1),哪怕只有一处也破坏原子性 - 32 位系统上
int64原子操作要求地址 8 字节对齐,Go 运行时通常保证,但若手动构造结构体字段顺序不当,可能触发 panic
atomic.CompareAndSwapInt32 实现无锁状态机
当需要“仅在满足某条件时才更新”(比如初始化一次、状态从 idle 切到 running),CAS 是核心原语。它比锁更细粒度,且天然支持乐观并发控制。
const (
stateIdle = iota
stateRunning
stateDone
)
var state int32 = stateIdle
func startWork() bool {
return atomic.CompareAndSwapInt32(&state, stateIdle, stateRunning)
}
func finishWork() {
atomic.StoreInt32(&state, stateDone)
}
func getState() int32 {
return atomic.LoadInt32(&state)
}
-
CompareAndSwap返回bool:成功则返回true,失败不修改值 - 必须用
int32(或uint32)配合CAS,int64版本在 32 位平台需要额外指令支持,性能略低 - 别写成
if state == stateIdle { state = stateRunning }—— 这中间存在竞态窗口,CAS才是正确抽象
指针和结构体的原子操作要格外小心
atomic.Pointer[T](Go 1.19+)可用于原子替换指针,但 atomic.Value 更适合存储任意类型数据(如配置快照)。二者都不支持对结构体字段做部分更新 —— 原子操作只能针对整个值。
立即学习“go语言免费学习笔记(深入)”;
-
atomic.Pointer[*MyStruct]可以安全地替换整个指针,但无法原子修改p.Load().Field = x -
atomic.Value的Store/Load是类型安全的,但内部用反射,有轻微开销;适合不频繁写、高频读的场景(如全局配置) - 没有
atomic.UpdateString或atomic.AddFloat64—— 浮点数和字符串需靠Value或自定义 CAS 循环实现
真正容易被忽略的是:原子操作只保证单个操作的线性化,不构成内存屏障之外的同步语义。如果后续逻辑依赖该原子操作的结果(比如写完 flag 再发消息),往往还需搭配 sync/atomic 提供的 Store/Load 内存序控制,或直接使用 chan / sync.WaitGroup 显式协调。










