普通int变量加锁做计数器性能差且易出错,应优先用atomic.addint64;需注意类型为int64、正确使用地址、避免cas误用,并封装为结构体方法以保障安全性和可扩展性。

为什么不能直接用普通变量加锁做计数器
普通 int 变量配合 sync.Mutex 虽然能保证线程安全,但在高并发下会成为性能瓶颈:锁竞争激烈,goroutine 频繁阻塞唤醒,吞吐量急剧下降。实测在 10k goroutine 并发下,sync.Mutex 版本比原子操作慢 5–10 倍,且 CPU 缓存行争用明显。
更隐蔽的问题是:如果计数逻辑里混入其他非原子操作(比如日志、条件判断),锁的粒度容易失控,反而掩盖真正的竞态点。
atomic.AddInt64 是最直接可靠的递增方式
Go 标准库的 atomic 包提供无锁原子操作,atomic.AddInt64 是实现递增计数器的首选——它底层对应 CPU 的 LOCK XADD 指令(x86)或 LDXR/STXR(ARM),硬件级保证可见性与顺序性。
- 必须使用
int64类型(atomic.AddInt32也可,但注意 32 位溢出风险) - 初始值需用
int64显式声明,避免类型推导错误:var counter int64 = 0 - 返回值是递增后的值,如需“先取再增”,得手动处理:
val := atomic.AddInt64(&counter, 1) - 1 - 不能对表达式取地址,比如
&counter + 1是非法的,只能传&counter
示例:
var counter int64
<p>func Inc() int64 {
return atomic.AddInt64(&counter, 1)
}</p><p>// 并发调用安全
for i := 0; i </p><h3>需要带重置或比较交换时用 <code>atomic.CompareAndSwapInt64</code>
</h3><p>单纯递增够用,但若业务要求“仅当当前值为 X 时才加 1”(比如限流器中的令牌桶重置),就得用 <code>atomic.CompareAndSwapInt64</code> 手动实现 CAS 循环。</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill6460" title="Golang Naming"><img
src="https://img.php.cn/upload/skill/000/000/081/179094616043400.jpg" alt="Golang Naming" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill6460" title="Golang Naming" class="overflowclass">Golang Naming</a>
<p class="overflowclass">Go(Golang)命名规范 — 包括包、构造函数、结构体、接口、常量、枚举、错误、布尔值、接收器、getter/setter、函数等。</p>
</div>
<a rel="nofollow" href="/xiazai/skill6460" title="Golang Naming" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div>
- CAS 不是万能的:失败时需重试,极端情况下可能饿死(虽概率极低)
- 不要在 CAS 循环里放耗时操作,否则拖慢整个原子路径
- 常见误写:
atomic.CompareAndSwapInt64(&counter, old, old+1)中old是旧值快照,必须在循环内重新读取,否则会丢失中间更新
正确写法:
func IncIfEven() bool {
for {
old := atomic.LoadInt64(&counter)
if old%2 != 0 {
return false
}
if atomic.CompareAndSwapInt64(&counter, old, old+1) {
return true
}
// CAS 失败,重试
}
}
全局计数器别忘了考虑初始化和导出问题
很多人把计数器定义成包级变量后,直接在 init() 里初始化,但忽略了两个实际问题:
- 如果多个包 import 了该计数器所在包,
init()执行顺序不可控,可能导致未初始化就读写 - 导出变量名首字母大写(如
Counter)会暴露给外部,但外部无法保证只读——应封装成函数接口,而非暴露int64变量本身 - 若需持久化或监控上报,建议把计数器包装进 struct,用方法控制访问,方便后续加 hook 或 metrics 上报
推荐模式:
type Counter struct {
val int64
}
<p>func (c *Counter) Inc() int64 {
return atomic.AddInt64(&c.val, 1)
}</p><p>func (c *Counter) Load() int64 {
return atomic.LoadInt64(&c.val)
}</p><p>var DefaultCounter = &Counter{}</p>
真正麻烦的是跨进程计数或需要持久化的场景——原子操作只解决单机内存并发,这时候得换方案,比如 Redis 的 INCR 或数据库的 UPDATE ... SET count = count + 1,但那就不是 atomic 能覆盖的范围了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










