直接用 int 类型做并发计数会出错,因为 ++ 操作非原子,底层拆为读取→加1→写回三步,多 goroutine 同时执行时可能读到相同旧值并写回相同新值,导致更新丢失;必须用 int64 配合 atomic.AddInt64 等原子操作确保安全。

为什么直接用 int 类型做计数器在并发下会出错
因为 int 的自增(++)不是原子操作:读取、加 1、写回三步,多个 goroutine 同时执行时会相互覆盖。比如两个 goroutine 同时读到值 5,各自加 1 写回 6,最终结果是 6 而不是 7。
常见错误现象包括:计数值明显偏小、增长不线性、压测时结果每次都不一致。这不是偶发 bug,而是必然发生的数据竞争。
Go 自带的 go run -race 能捕获这类问题,但不能代替正确实现。
用 sync/atomic 实现零锁计数器
sync/atomic 提供了真正原子的整数操作,性能远高于互斥锁,且无需额外内存分配。
立即学习“go语言免费学习笔记(深入)”;
- 对
int64计数器,用atomic.AddInt64(&counter, 1)替代counter++ - 读取必须用
atomic.LoadInt64(&counter),不能直接读变量 - 初始化必须是
int64类型(哪怕只计小数字),否则atomic函数 panic - 注意:所有操作都要求变量地址对齐,全局变量或 struct 字段首地址天然对齐;局部变量(如函数内定义)地址不一定对齐,慎用
示例:
var counter int64
func Inc() {
atomic.AddInt64(&counter, 1)
}
func Get() int64 {
return atomic.LoadInt64(&counter)
}
什么时候该用 sync.Mutex 而不是 atomic
当计数逻辑不止“加一”,还涉及条件判断、复合更新或非原子类型(如 map、string、struct)时,atomic 就不够用了。
- 比如:“仅当当前值小于 100 时才加 1”——
atomic.CompareAndSwapInt64可行,但逻辑变复杂 - 再比如:“统计请求次数 + 记录最近 5 次时间戳”——必须用
sync.Mutex或sync.RWMutex保护整个结构体 -
sync.Mutex的开销比atomic高一个数量级,但在复杂逻辑中更易维护和避免竞态
别为了“看起来快”硬套 atomic,可读性和正确性优先。
别忽略初始化和导出规则
Go 包级变量默认零值,但如果你把计数器封装进 struct 并导出方法,就得小心字段可见性与并发安全边界。
- 导出字段(首字母大写)如
Count int64,外部可直接读写,atomic失效——必须设为未导出字段(count int64)并只提供原子方法 - 不要在 init 函数里用
atomic.StoreInt64初始化,直接赋值即可(count: 0),因为写入本身是原子的 - 如果计数器需重置,用
atomic.StoreInt64(&counter, 0),而非赋值语句
最易被忽略的一点:跨包使用时,确保所有对同一计数器变量的访问路径都走原子操作——漏掉一处 counter++,整个并发安全就崩了。


















