直接用 int 类型做计数器在 goroutine 中会出错,因为 i++ 不是原子操作,底层拆为读-改-写三步,多 goroutine 并发时可能丢失更新,导致最终值远小于预期且结果不一致。

为什么直接用 int 类型做计数器在 goroutine 里会出错
因为 int 的自增(i++)不是原子操作,底层拆成「读-改-写」三步。多个 goroutine 同时执行时,可能都读到旧值、各自加 1、再写回去,结果只 +1 而不是 +n。现象通常是最终值远小于预期,且每次运行结果不一致。
常见错误写法:
var count int<br>go func() { count++ }()<br>go func() { count++ }()——这几乎必然丢数据。
- 别依赖编译器优化或运气,Go 不保证这种操作的线程安全性
- 哪怕只读不写,多个 goroutine 并发读
int虽不会 panic,但若同时有写入,仍属未定义行为 - 用
sync.Mutex能解决,但锁开销大,尤其高并发读多写少场景
用 sync/atomic 替代锁是最轻量的方案
sync/atomic 提供无锁原子操作,底层映射到 CPU 的 CAS 或 XADD 指令,性能比 sync.Mutex 高一个数量级。适用于整数型计数器(int32、int64、uint32 等)。
关键点:
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 必须用指针传参:
atomic.AddInt64(&count, 1),不能传值 - 变量类型要对齐:32 位系统上
int64必须 8 字节对齐,否则 panic 报panic: atomic operation not allowed on unaligned pointer - 推荐声明为
int64:避免 32 位平台溢出风险,且 Go 默认对int64做内存对齐 - 读取用
atomic.LoadInt64(&count),不要直接读变量——虽然有时看起来“能读”,但不保证看到最新值
封装成结构体让调用更安全、可复用
裸用 atomic 函数容易漏掉指针符号或类型不匹配。封装一层能约束使用方式,也方便后续扩展(比如加 reset、CAS 判断等)。
示例:
type Counter struct {<br> val int64<br>}<br><br>func (c *Counter) Add(n int64) {<br> atomic.AddInt64(&c.val, n)<br>}<br><br>func (c *Counter) Get() int64 {<br> return atomic.LoadInt64(&c.val)<br>}
- 字段
val不导出,强制走方法访问,避免误操作 - 不要给
Get()加锁或用atomic.Load以外的方式——否则失去原子性意义 - 如果需要支持负数减法,直接用
Add(-n),不用单独写Sub方法 - 注意:该结构体本身不是线程安全的「零值」——必须显式初始化,
var c Counter是合法的,c.val默认为 0
框架里已有原子计数器?优先用现成的
像 prometheus 的 prometheus.CounterVec 或 go.uber.org/zap 日志库里的内部计数器,底层其实也基于 sync/atomic。如果你已在用这些框架,别重复造轮子。
-
prometheus.NewCounter返回的是接口,其Inc()方法是并发安全的,适合监控指标类计数 - 但它的设计目标是暴露 metrics,不适合做业务逻辑中的中间状态计数(比如限流令牌数),因为不可读、不可重置
- 有些 ORM 或 RPC 框架(如
ent、grpc-go)会在 stats 或 middleware 中内置计数器,查文档确认是否开放了读写接口 - 若框架没提供,或者你需要极简、零依赖的实现,
sync/atomic+ 封装结构体仍是最佳选择
真正容易被忽略的是内存对齐和指针传递——写错一个 & 符号,或把 int32 当 int64 用,程序可能跑几天才偶然崩一次,排查成本远高于写对的成本。

















