不能直接用int做并发计数,因++或+=是非原子的“读-改-写”三步操作,必致data race;必须用atomic.AddInt64(&counter, 1)等原子函数,且变量须为int64类型、地址稳定、8字节对齐,并全程统一使用atomic读写。

为什么不能直接用 int 做并发计数
多个 goroutine 同时对一个普通 int 变量执行 ++ 或 +=,会触发“读-改-写”三步非原子操作:先读当前值,再加 1,最后写回。中间任意时刻被抢占,就可能丢失一次更新。现象是最终结果小于预期(比如 1000 个 goroutine 各加 1,结果却只有 987)。这不是偶发 bug,而是数据竞争的必然表现。
解决思路只有两个:加锁(sync.Mutex)或用原子操作。前者有调度开销,后者在简单计数场景更轻量。
atomic.AddInt64 是最常用且安全的选择
Go 的 sync/atomic 要求操作变量必须是导出的、对齐的整型变量,且必须用指针传入。最稳妥的做法是声明为 int64 类型(32 位系统上 int 长度不固定,atomic 不保证其原子性)。
- 初始化必须用
var counter int64,不能是短变量声明counter := int64(0)(后者是局部变量,地址不可靠) - 递增:调用
atomic.AddInt64(&counter, 1),返回新值;若只需副作用,忽略返回值即可 - 读取当前值必须用
atomic.LoadInt64(&counter),不能直接读counter——否则仍可能读到撕裂值(尤其在 32 位系统读 64 位值时) - 重置可使用
atomic.StoreInt64(&counter, 0)
示例片段:
var counter int64
go func() {
atomic.AddInt64(&counter, 1)
}()
// ... 其他 goroutine
fmt.Println(atomic.LoadInt64(&counter)) // 安全读取
别踩这些坑:指针、类型、内存对齐
常见错误不是函数用错,而是变量本身不满足原子操作前提:
-
atomic操作要求变量地址是自然对齐的(如int64需 8 字节对齐)。结构体字段顺序不当可能导致不对齐,例如:type T struct { A byte; B int64 }中B很可能未对齐。应把大类型放前面:type T struct { B int64; A byte } - 切片、map、struct 本身不能原子操作,只能操作其中的某个
int64字段,且该字段必须是导出的(首字母大写)或位于顶层包级变量中 - 不要对函数返回值或临时变量取地址:
atomic.AddInt64(&(x+y), 1)是非法的,编译不通过 - 32 位系统上,对
int64的原子操作依赖底层 CPU 支持(如 x86 上用cmpxchg8b),但 Go 运行时已封装处理,无需额外判断
何时该换回 sync.Mutex
原子操作只适合单一整型的简单读写。一旦涉及多个变量联动,或需要条件判断后再更新(如“如果值小于 100 才加 1”),就不能靠 atomic 安全完成。
-
atomic.CompareAndSwapInt64能做 CAS,但逻辑复杂时极易写出 ABA 问题或忙等死循环 - 读取后做计算再写回(如
counter = max(counter, threshold))无法拆成原子步骤,必须用锁保护临界区 - 如果计数器只是更大状态的一部分(比如和一个
sync.Map更新耦合),统一用锁反而更清晰、不易出错
原子操作不是银弹,它省掉的是锁的调度成本,换来的是对使用边界的严格约束——越简单,越可靠。

















