不能直接用普通变量做并发计数,因为++操作非原子,包含读取、加1、写回三步,多goroutine并发时会因竞态导致结果错误且不可预测;应使用int64类型配合atomic.AddInt64和atomic.LoadInt64实现安全计数。

为什么不能直接用普通变量做并发计数
多个 goroutine 同时对一个 int 变量执行 ++,结果大概率出错——这不是“偶尔不准”,而是根本不可预测。因为 ++ 实际包含读取、加 1、写回三步,中间可能被其他 goroutine 插入,导致覆盖或丢失更新。哪怕只差一次,统计就失真;而线上服务跑几天后偏差几百上千很常见。
- 典型错误现象:
counter最终值远小于预期(比如启动 1000 个 goroutine 各加 1,结果只有 600+) - 调试困难:问题不总复现,且无法通过加
fmt.Println暴露(打印本身会改变调度节奏) - 锁方案(
sync.Mutex)能解决,但有额外开销,尤其高竞争场景下性能明显下降
atomic.AddInt64 是最常用也最稳妥的计数方式
Go 的 sync/atomic 包提供无锁原子操作,atomic.AddInt64 就是专为计数设计的:它保证“读-改-写”整套动作不可中断,且底层直接映射到 CPU 的 LOCK XADD 等指令,比互斥锁快一个数量级。
- 必须用
int64类型(不是int),否则编译报错:cannot use counter (type int) as type int64 in argument to atomic.AddInt64 - 初始化要显式声明为
int64:var counter int64 = 0或counter := int64(0) - 递增写法固定:
atomic.AddInt64(&counter, 1)—— 注意传的是指针,不是值 - 读取也要用原子操作:
atomic.LoadInt64(&counter),不能直接counter,否则可能读到中间态
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var counter int64
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt64(&counter, 1)
}()
}
wg.Wait()
fmt.Println("count:", atomic.LoadInt64(&counter)) // 总是 1000
}
需要自定义行为时,atomic.CompareAndSwapInt64 更灵活但更难写对
如果计数逻辑不是简单加减(比如“只在值小于 100 时才加 1”),就得用 CAS(Compare-And-Swap)。它不保证成功,需循环重试,容易写出死循环或漏更新。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 典型误用:忘记检查返回值,导致条件判断失效
- 正确模式是“读当前值 → 计算新值 → CAS 尝试更新 → 失败则重读”,必须用
for循环包裹 - CAS 在高冲突下性能不如
AddInt64,仅当逻辑复杂且无法拆解时才用 - 示例场景:实现带上限的计数器,超限后停止累加
func incWithLimit(counter *int64, limit int64) {
for {
old := atomic.LoadInt64(counter)
if old >= limit {
return
}
if atomic.CompareAndSwapInt64(counter, old, old+1) {
return
}
// CAS 失败,说明别人已修改,继续下一轮
}
}
别忽略 atomic 的内存序和跨平台限制
Go 的 atomic 默认提供最强顺序保证(sequential consistency),多数场景够用,但极少数高性能代码需手动控制内存序(如 atomic.LoadInt64Acquire)。不过日常开发几乎用不到——强行优化反而易出错。
立即学习“go语言免费学习笔记(深入)”;
- 所有
atomic函数只支持固定类型:int32、int64、uint32、uint64、uintptr、*unsafe.Pointer和布尔值;不支持结构体或自定义类型 - 32 位系统上
atomic.AddInt64仍可用,但底层可能用锁模拟,性能略降(现代服务器基本都是 64 位,可忽略) - 切记:原子操作只保单个变量的线程安全,若需多个变量同步更新(如计数器 + 时间戳),还得回退到
sync.Mutex
atomic.AddInt64 + atomic.LoadInt64 就够了;真正卡点的往往是没意识到必须用 int64,或者读取时忘了调用 LoadInt64。

















