首选 sync/atomic 实现零锁计数器,因其映射 cpu 原子指令、无调度开销;仅适用于整数增减且无需复合操作;需确保地址对齐、位宽匹配,不支持 float 原子操作。

用 sync/atomic 实现零锁计数器
最轻量、最高性能的并发安全计数器,首选 sync/atomic。它底层直接映射到 CPU 原子指令(如 ADDQ),不涉及 Goroutine 调度或锁竞争。
适用场景:只做整数增减(int32 / int64)、无需复合操作(比如“读+写”判断)、不需要阻塞等待。
常见错误:对非对齐地址调用原子操作(如结构体中未对齐的 int32 字段),或混用不同位宽(atomic.AddInt32 传入 *int64)——会 panic 或触发 undefined behavior。
-
atomic.AddInt64(&counter, 1)是线程安全的自增,返回新值;atomic.LoadInt64(&counter)安全读取 - 必须确保变量地址对齐:全局变量或结构体首字段天然对齐;若嵌套在 struct 中,建议用
int64且加//go:notinheap注释避免逃逸干扰对齐 - 不支持 float 类型原子操作,需转为
uint64用atomic.Uint64(Go 1.19+)或手动 bitcast
用 sync.Mutex 包裹普通变量
当需要条件判断、多字段协同更新(如“仅当小于阈值才递增”)、或配合其他非原子逻辑时,sync.Mutex 是最直接可控的选择。
性能代价明显高于 atomic,但在逻辑复杂时反而更清晰、更不易出错。
容易踩的坑:忘记 Unlock() 导致死锁;在 defer 中 unlock 却提前 return 引发 panic;或把 mutex 做成值拷贝(如作为 struct 字段被复制)——副本的 lock 状态丢失,失去保护作用。
- mutex 必须是 pointer 或嵌入 struct 的指针接收方法中使用:
type Counter struct { mu sync.Mutex; n int64 } - 推荐写法:
func (c *Counter) Inc() { c.mu.Lock(); defer c.mu.Unlock(); c.n++ } - 避免在循环内反复 Lock/Unlock;若批量操作,可一次锁定完成多个修改
用 sync.Once 和 sync.WaitGroup 不适合做计数器
这两个类型常被误用于计数场景,但语义和行为完全不匹配:
-
sync.Once只保证函数执行一次,无法递增、无法读取当前值,Do()后无状态暴露 -
sync.WaitGroup专为“等待一组 goroutine 结束”设计,Add()和Done()是配对操作,不能负向调整,也不提供Load()接口;调用Add(n)后未调用足够Done()会导致Wait()永久阻塞 - 强行 hack(比如用
WaitGroup的内部字段)违反封装,Go 运行时版本升级可能直接崩溃
何时该用 chan int?基本不该
有人想用带缓冲 channel 模拟计数器(ch 表示 +1,<code>len(ch) 当前值),这属于典型反模式:
-
len(ch)不是原子操作,且仅反映缓冲区长度,与业务语义脱节 - channel 创建开销远大于 mutex,内存占用高(每个 channel 至少几百字节)
- 无法做条件更新(比如“如果当前值
- goroutine 泄漏风险高:发送端关闭 channel 后,接收端未处理完就退出,残留消息无处安放
channel 的核心价值是通信与同步,不是共享状态存储。真要靠 channel 协调计数,说明架构已偏离 Go 的并发哲学。
真正难的不是选哪个 API,而是判断“这个计数是否真的需要跨 goroutine 共享”。很多所谓“并发计数器”,其实只是日志统计或监控指标——这类场景用 per-Goroutine 局部计数 + 定期汇总到中心 collector,往往更高效、更易观测。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











