int自增非原子操作,多goroutine并发时会因读-加-写分离导致覆盖;应使用sync/atomic包的atomic.AddInt64等函数,配合atomic.LoadInt64读取,确保内存可见性与原子性。

为什么不能直接用 int++ 做计数器
因为 int 的自增不是原子操作:读取、加 1、写回三步分开执行,多 goroutine 并发时大概率覆盖彼此结果。你看到计数器“卡在某个值不动”或“增长比预期少”,基本就是这个原因。
Go 的 sync/atomic 包提供真正原子的整数操作,底层靠 CPU 指令(如 x86 的 XADD)保证单条指令完成读-改-写,不需要锁。
注意:int 类型本身不支持原子操作;必须用 int32 或 int64,且变量需对齐(通常声明为全局或结构体首字段即可,Go 编译器一般自动处理)。
用 atomic.AddInt64 实现安全递增
这是最常用的原子计数方式,返回新值(不是旧值),适合做“当前总数”统计。
立即学习“go语言免费学习笔记(深入)”;
-
atomic.AddInt64第二个参数是增量,支持负数(即减法),比如atomic.AddInt64(&counter, -1) - 变量必须是指针,传入
&counter,不能传值 - 别混用
int32和int64版本——atomic.AddInt32和atomic.AddInt64操作的是不同内存位置,无法互斥 - 示例:
var counter int64 go func() { atomic.AddInt64(&counter, 1) }()
读取计数器要用 atomic.LoadInt64,不是直接读变量
看似简单的 counter 直接读取,在某些架构(如 ARM)或编译器优化下可能读到陈旧值。Go 不保证普通变量读写的内存可见性顺序。
必须用 atomic.LoadInt64(&counter) 才能确保读到最新写入的结果,同时建立 happens-before 关系。
- 不要写
fmt.Println(counter)—— 这是普通读,不可靠 - 也不要写
atomic.StoreInt64(&counter, 0)来清零,除非你真需要原子写;单纯设初值用counter = 0即可(初始化阶段无并发) - 如果要“读+重置”,没有原子的 read-and-reset,得用
atomic.SwapInt64(&counter, 0),它返回旧值并设新值
什么时候该换 sync.Mutex 而不是硬扛原子操作
原子操作快,但只适合简单整数运算。一旦逻辑变复杂,比如“加 1 但不超过阈值”“先读再条件更新”,就容易写出竞态或死循环。
例如想实现带上限的计数器:
// ❌ 错误:非原子的“读-判-写”三步有竞态
if atomic.LoadInt64(&counter) < 100 {
atomic.AddInt64(&counter, 1)
}
这中间可能被其他 goroutine 插入修改,导致超限。此时应改用 sync.Mutex 包裹整个判断+更新逻辑。
原子操作不是银弹;它省掉锁开销,但也牺牲了表达力。能用 atomic 就用,但别为了“无锁”强行拆解业务逻辑。
对齐、类型、读写配对——这三个点漏掉任何一个,都可能让原子计数器在某次发布后突然失准,而且很难复现。











