Go 的 atomic 包可安全实现无锁计数器,但必须严格使用匹配类型(如 int64)、正确函数(如 atomic.AddInt64)和内存序,且变量需对齐、禁止非原子访问,否则仍会出错。

Go 的 atomic 包能安全实现无锁计数器,但必须用对类型、函数和内存序,否则仍可能出错。
为什么不能直接用 int 加 sync.Mutex?
用 sync.Mutex 虽然安全,但在高并发自增场景下会成为性能瓶颈;而裸写 int(比如 i++)在多 goroutine 下是未定义行为——它由读、加、写三步组成,中间可能被抢占,导致丢失更新。Go 不保证这类操作的原子性,哪怕在 64 位机器上对 int64 做 ++ 也不行。
- 必须使用
atomic提供的专用函数,如atomic.AddInt64 - 底层变量必须是对齐的(
int64在 64 位系统上天然对齐,但struct中嵌套需小心) - 不能取地址后传给非 atomic 函数(比如传给
fmt.Printf("%d", &x)是错的)
atomic.AddInt64 是最常用也最易误用的函数
它返回新值(不是旧值),且要求操作数是指向 int64 的指针。常见错误是传入 int 或未取地址:
var count int64 // ✅ 正确:显式声明为 int64,取地址 atomic.AddInt64(&count, 1) // ❌ 错误:int 不等于 int64(32 位平台尤其危险) var x int = 0 atomic.AddInt64(&x, 1) // 编译失败:*int 不能转 *int64 // ❌ 错误:忘了取地址 atomic.AddInt64(count, 1) // 编译失败:int64 不能转 *int64
- 所有
atomic数值操作都只支持固定宽度类型:int32、int64、uint32、uint64、uintptr,没有int - 如果业务逻辑需要初始值非零,直接赋值即可:
count := int64(100),再传&count - 不要试图用
atomic.LoadInt64+ 普通加法替代AddInt64,那不是原子的
计数器需要读取时,优先用 atomic.LoadInt64 而非直接读变量
虽然 Go 内存模型允许对单一 int64 的读写有“看起来像原子”的效果,但这不保证可见性与顺序。多个 goroutine 并发读写时,不加 atomic 读可能导致看到撕裂值或过期缓存值。
-
atomic.LoadInt64(&count)确保读到最新写入值,并建立 happens-before 关系 - 避免混用:
count变量本身不应被非 atomic 方式读写(包括fmt.Println(count)) - 如果只是调试打印,可临时用
atomic.LoadInt64,但生产代码中应统一抽象为方法,比如c.Value()
封装成结构体时注意字段对齐和导出控制
把计数器封装成结构体更易复用,但要注意:未导出字段+非对齐布局可能破坏原子性保障。
type Counter struct {
// ✅ 推荐:首字段即原子变量,自然对齐
n int64
}
func (c *Counter) Inc() { atomic.AddInt64(&c.n, 1) }
func (c *Counter) Value() int64 { return atomic.LoadInt64(&c.n) }
// ❌ 危险:前面有小字段导致 n 不对齐(尤其在 32 位环境或 struct 混合小类型时)
type BadCounter struct {
name string // 可能占 16 字节(含 header),使 n 偏移非 8 倍数
n int64
}
- Go 1.19+ 对
atomic类型做了更强检查,但不保证跨平台一致;最稳妥是让原子字段位于结构体开头 - 如果必须带其他字段,可用
_ [0]uint64填充对齐,或用go vet -atomic检查 - 不要为
int64字段加json:tag 后直接序列化——json.Marshal会反射读取原始字段,绕过 atomic 语义
真正难的不是调用哪个函数,而是意识到:只要有一次非 atomic 访问,整个无锁假设就崩了。别依赖“好像没出过错”。


















