高并发计数器必须用sync/atomic或sync.mutex,不能裸用int64;复合操作(如先读再改、多字段协同)必须用mutex,简单增减推荐atomic.int64(go 1.19+),多key统计优先sync.rwmutex+map。

高并发下计数器必须用 sync/atomic 或 sync.Mutex,不能裸用 int64 变量或 counter++;选哪个不看“并发量大小”,而看“操作逻辑是否复合”。
用 atomic.Int64 实现零锁、高性能计数
Go 1.19+ 推荐直接使用 atomic.Int64 类型,它比裸调 atomic.AddInt64(&x, 1) 更安全:编译器能强制内存顺序、避免重排、杜绝类型误用。
- 声明必须是
var counter atomic.Int64,不是var n int64后传地址——后者在竞态检测松懈时可能读到未初始化值或被优化掉同步语义 - 增减统一走
counter.Add(1),读取用counter.Load(),重置用counter.Store(0) - 初始化无需额外操作,
atomic.Int64{}零值即为 0,但若需非零初值(如 -1 表示未就绪),必须用Store()显式写入 - 结构体中嵌入时,把它放在第一个字段,或加
//go:notinheap注释防止逃逸干扰对齐(尤其在struct{ mu sync.Mutex; val atomic.Int64 }这类组合里)
当需要“先读再改”时,sync.Mutex 是唯一靠谱选择
atomic.CompareAndSwapInt64 看似能支持条件更新,但实际容易陷入自旋、饥饿或逻辑错乱。真实业务中,“仅当小于阈值才递增”“超限后记录拒绝次数”这类需求,必须用锁包住整个判断块。
- 所有读写操作都得进临界区:
mu.Lock()→ 检查条件 → 修改 →mu.Unlock(),中间不能穿插任何非原子操作 - 别把
sync.Mutex字段做成值拷贝:结构体返回值、函数参数传值、map 中存 struct 值,都会导致副本的mu失去保护作用 - 如果计数器要附带时间戳、状态标记等多字段协同更新,
Mutex封装的结构体天然适合,而atomic无法跨字段保证原子性 - 高频写场景下,锁开销确实存在,但每秒十万级以下计数基本感知不到;真到百万级,应先确认瓶颈是否真在计数器本身(比如日志打点、HTTP header 解析更可能是瓶颈)
多 key 统计别裸用 map,优先 sync.RWMutex + 普通 map
多个 goroutine 并发写同一个 map[string]int64,运行时会直接 panic:“concurrent map writes”。这不是概率问题,是 Go 主动终止程序防止数据损坏。
-
sync.Map不是银弹:它不支持range,遍历必须用Range()回调;值是interface{},每次读都要类型断言;键频繁增删时性能反而不如加锁map - 读远多于写(比如监控指标每秒查 1000 次、只写 5 次),用
sync.RWMutex:读用RLock(),允许多个 goroutine 并发;写仍用Lock()独占 - 写密集场景(如实时错误码聚合),
sync.Mutex+map更简单可控;若担心锁争抢,可考虑分片(sharding):按 key 哈希到 N 个子 map + N 个独立 mutex - 无论哪种方案,都必须封装成结构体暴露方法(如
Inc(key string)、Get(key string) int64),禁止外部直接访问内部map或mu
最容易被忽略的是:哪怕用了 atomic.Int64,只要你在临界区外做任何依赖其值的决策(比如根据计数值决定是否触发告警),就必须确保该决策与计数更新之间有明确的 happens-before 关系——这通常意味着要配合 atomic.Store/Load 或退回到 Mutex。原子操作保的是单条指令,不是你的业务逻辑。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











