为什么不能直接用 int 类型做并发计数
多个 goroutine 同时执行 counter++ 时,底层会拆成「读取→加 1→写回」三步,中间可能被其他 goroutine 插入,导致丢失更新。哪怕只差一个请求,对限流、配额、监控指标这类场景都是致命的。
常见错误现象:go run main.go 启动 100 个 goroutine 各自调用 100 次 counter++,最终结果却远小于 10000;go build -race 会报出 Data race 警告。
- 不要用
sync.Mutex包裹简单计数——锁开销大,尤其高并发下成为瓶颈 - 避免用
channel做计数器——语义不符、阻塞风险、内存占用高 - 别把
atomic.LoadInt64(&counter)和atomic.AddInt64(&counter, 1)拆开用——中间可能被抢占,破坏原子性
atomic.AddInt64 是最直接可靠的递增方式
atomic.AddInt64 是硬件级原子指令,在 x86-64 上对应 lock xadd,无锁、无调度、无内存分配,单次调用耗时稳定在纳秒级。
使用场景:HTTP 请求计数、API 调用频次统计、熔断器请求数采集。
var counter int64
func inc() {
atomic.AddInt64(&counter, 1)
}
func get() int64 {
return atomic.LoadInt64(&counter)
}
-
&counter必须传地址,且变量生命周期需覆盖所有 goroutine 使用期(全局变量或结构体字段最稳妥) - 不要用
int或uint32——atomic包对 32 位整数有平台限制(ARM32 不保证原子性),一律用int64或uint64 - 如果需要带条件的递增(比如仅当值小于阈值才加),得换
atomic.CompareAndSwapInt64,但注意它不自动重试,需手动循环
如何安全地重置计数器并获取旧值
没有原子的「清零并返回原值」操作,必须组合 Load + Store,但中间存在竞态窗口。正确做法是用 Swap——它一步完成读+写,且返回旧值。
典型需求:每分钟上报一次请求数并归零,避免漏报或重复报。
func resetAndGet() int64 {
return atomic.SwapInt64(&counter, 0)
}
-
atomic.SwapInt64(&counter, 0)是原子的,不会被其他 goroutine 中断 - 不要写成
v := atomic.LoadInt64(&counter); atomic.StoreInt64(&counter, 0)——两步之间可能有其他 goroutine 修改了counter,导致返回值不准 - 如果需要「减去某个值并返回原值」,没有现成函数,只能用
CompareAndSwap循环实现,慎用
计数器嵌入结构体时要注意字段对齐
当把 int64 计数器作为结构体字段时,若前面字段长度不是 8 字节倍数,会导致该字段地址不对齐,某些平台(如 ARM64)上 atomic 操作 panic。
错误示例:type Stats struct { name string; reqs int64 } 中 name 占 16 字节(指针+len+cap),reqs 地址自然对齐;但换成 id int32 就危险了。
- 用
go vet可检测部分对齐问题,但不覆盖全部场景 - 最稳方案:用
_ uint64填充,或把int64字段放在结构体开头 - 运行时可通过
unsafe.Alignof(counter)和unsafe.Offsetof(s.reqs)验证是否对齐
unsafe.Offsetof,也不要等压测时发现计数少了 3%。











