atomic.addint64是唯一安全的递增方式,因counter++非原子必致数据竞争;必须用int64类型、传指针&counter、所有读写统一走atomic函数(如loadint64/storeint64),否则可能读陈旧值或panic。

atomic.AddInt64 是唯一安全的递增方式
直接写 counter++ 或 counter += 1 必然导致数据竞争——这不是“可能出错”,而是只要并发执行,结果就不可信。Go 的 -race 检测器会立刻报 Data Race,不加检测时也常出现计数“卡住”或远低于预期值(比如 1000 次递增只得到 923)。
atomic.AddInt64 才是真正原子的:它编译为单条 CPU 指令(如 x86 的 XADD),读、加、写一步完成,无中间状态。但必须满足三个硬性条件:
- 变量类型必须是
int64(不能用int,32 位系统上长度不固定,atomic不保证其原子性) - 传参必须是指针:
&counter,传值会编译失败 - 所有访问都得走
atomic函数——哪怕只是读,也必须用atomic.LoadInt64(&counter),直接读counter可能拿到陈旧值(尤其在 ARM 或优化开启时)
读取和重置必须匹配 atomic 函数
很多人以为“只写用 atomic,读随便读”没问题,实际这是最常被忽略的坑。普通读取不建立 happens-before 关系,编译器或 CPU 可能重排指令,导致看到撕裂值(32 位系统读 64 位值时甚至 panic)。
正确做法:
- 读当前值 → 用
atomic.LoadInt64(&counter) - 设初值(初始化阶段无并发)→ 直接
counter = 0即可,不用atomic.StoreInt64 - 运行中清零 → 用
atomic.StoreInt64(&counter, 0) - 读+清零(原子)→ 用
atomic.SwapInt64(&counter, 0),它返回旧值并设新值
别写 fmt.Println(counter),也别在循环里缓存 atomic.LoadInt64 的结果当条件判断依据——值可能已变。
什么时候必须换 sync.Mutex?
atomic 不是万能的。一旦逻辑超出“单个整数的纯增减”,就该切回锁。典型场景包括:
- 带条件更新:比如“如果
counter 才加 1”,<code>if atomic.LoadInt64(&counter) 是竞态的——两 goroutine 同时通过判断,都会执行加法,超限 - 多字段联动:计数器 + 时间戳 + 状态码要一起更新,
atomic无法保证它们的原子性一致 - 需要阻塞等待:比如“等
counter == 10再继续”,atomic不提供等待机制,只能轮询或换sync.Cond - 操作非整型/指针类型:
float64、map、结构体字段——atomic不支持,得用atomic.Value或锁
CAS 不是炫技,但得防忙等
atomic.CompareAndSwapInt64 真有用,比如服务启动一次、状态机切换、无锁栈 push。但它不是“自动重试”,失败后必须手动循环:
for {
old := atomic.LoadInt64(&state)
if old != 0 {
return // 已启动
}
if atomic.CompareAndSwapInt64(&state, 0, 1) {
break
}
runtime.Gosched() // 防止忙等占满 CPU
}
关键点:
- 每次 CAS 前必须重新
Load,不能复用旧值 - 失败时不 sleep,用
runtime.Gosched()让出时间片更轻量 - 32 位系统上
int64的 CAS 开销略大,高频场景优先用int32
最容易被忽略的一点:只要对某个变量启用了 atomic 操作,它所有的读写——无论在哪、谁写的——都必须走 atomic 函数。混用普通读写,等于没做任何保护。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











