不能直接用普通 int 做并发计数,因为 counter++ 是非原子的“读-改-写”三步操作,会导致数据竞争、计数值偏小、偶发 panic 或负数;应使用 sync/atomic.AddInt64 等原子操作,操作对象须为对齐的 int64 指针。

Go 语言中用 sync/atomic 实现并发安全计数器,比加锁更轻量、更高效,但必须严格遵循原子操作的使用边界——不能用在需要复合逻辑(如“读-改-写”非单条原子指令)的场景。
为什么不能直接用普通 int 做并发计数
多个 goroutine 同时执行 counter++ 会导致数据竞争:该操作实际拆分为「读取值 → 加 1 → 写回」三步,中间可能被其他 goroutine 打断。Go 的 go run -race 会立刻报出 Data race 错误。
常见错误现象:
- 最终计数值远小于预期(例如启动 1000 个 goroutine 各自 +1,结果只有 300+)
- 程序偶发 panic 或返回负数(因未对齐内存读写)
-
go build -race检测到Read at ... by goroutine N / Previous write at ... by goroutine M
sync/atomic.AddInt64 正确用法
这是最常用、最稳妥的并发计数方式。注意:操作对象必须是 int64 类型指针,且变量需对齐(全局变量或 struct 字段按 8 字节对齐即可)。
使用场景:
- 统计请求数、错误数、活跃连接数等纯增量指标
- 作为轻量信号量(如用 -1 表示关闭,用 atomic.CompareAndSwapInt64 切换状态)
- 配合
atomic.LoadInt64实现无锁读取
参数差异:
-
atomic.AddInt64(&counter, 1)返回新值;atomic.AddInt64(&counter, -1)可减 - 不要传局部变量地址(如
func() { n := int64(0); atomic.AddInt64(&n, 1) }),逃逸分析可能引发隐患 - 32 位系统上
int非原子安全,务必显式用int64并调用对应函数
var counter int64
func increment() {
atomic.AddInt64(&counter, 1)
}
func get() int64 {
return atomic.LoadInt64(&counter)
}
// 启动 100 个 goroutine 并发调用 increment()
for i := 0; i < 100; i++ {
go increment()
}
time.Sleep(time.Millisecond)
fmt.Println(get()) // 稳定输出 100
CompareAndSwapInt64 用于条件更新
当计数逻辑含判断(如“仅当当前值
容易踩的坑:
- 忘记循环:CAS 失败后不重试,逻辑就丢了
- 用错旧值:第二个参数必须是「你认为当前应该有的值」,不是固定常量
- 性能陷阱:高冲突下反复重试可能比 mutex 还慢,此时应考虑分片计数器(sharded counter)
func incrementIfLessThan(max int64) bool {
for {
old := atomic.LoadInt64(&counter)
if old >= max {
return false
}
if atomic.CompareAndSwapInt64(&counter, old, old+1) {
return true
}
// CAS 失败,说明期间有别的 goroutine 改了值,重试
}
}
真正要注意的是内存顺序和对齐——atomic 操作默认提供 sequential consistency,够用;但若嵌套在复杂结构体中,要确保字段偏移是 8 的倍数,否则在某些架构上 panic。这点常被忽略,尤其当计数器和其他字段混排时。










