不能直接用 int64 做并发计数,因为多个 goroutine 同时读写会触发数据竞争,导致未定义行为;应使用 sync/atomic 提供的原子操作如 atomic.addint64 安全增减。

为什么不能直接用 int64 做并发计数
Go 中多个 goroutine 同时读写一个 int64 变量(比如 counter++)会触发数据竞争,运行时加 -race 会立刻报错:fatal error: concurrent map writes(虽然这里不是 map,但 race detector 会标记所有未同步的非原子读写)。这不是“偶尔出错”,而是未定义行为——可能得到错误值、崩溃,或看似正常实则不可靠。
常见错误写法:
var counter int64
go func() { counter++ }()
go func() { counter++ }()
// 结果不一定是 2,且 -race 会报警
用 sync/atomic 实现轻量级原子计数器
sync/atomic 是最常用、开销最小的方案,适合只做增减、读取、比较交换等简单操作的场景。它底层调用 CPU 原子指令,无锁、无调度开销。
-
atomic.AddInt64(&counter, 1)返回新值;atomic.LoadInt64(&counter)安全读取 - 注意:所有操作必须传指针,且变量生命周期需保证不被 GC 提前回收(全局变量或结构体字段最稳妥)
- 不支持复合操作,比如「如果大于 100 就重置」需配合
atomic.CompareAndSwapInt64手动实现循环重试
示例:
var counter int64 for i := 0; i <h3>用 sync.Mutex 保护更复杂的计数逻辑</h3> <p>当计数器需要附带状态判断、条件重置、或与其他变量联动更新时,<code>atomic</code> 表达力不足,这时用 <code>sync.Mutex</code> 更清晰安全。</p>
- 把计数器封装成结构体,避免裸变量暴露
-
Mutex.Lock()必须配对Unlock(),推荐用defer mu.Unlock()防遗漏 - 性能比
atomic差,但在几十万 QPS 以下通常感知不到;高竞争下可能成为瓶颈
示例:
type Counter struct {
mu sync.Mutex
n int64
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.n++
}
func (c *Counter) Value() int64 {
c.mu.Lock()
defer c.mu.Unlock()
return c.n
}
别忽略零值和初始化陷阱
Go 中 sync.Mutex 和 atomic 类型变量本身是可直接使用的零值,但容易误以为需要显式初始化:
-
var mu sync.Mutex✅ 正确,零值可用;mu := sync.Mutex{}也行,但没必要 -
var counter int64✅ 可直接传给atomic函数;但若声明为局部变量且没逃逸,地址可能无效(编译器通常会阻止) - 结构体中嵌入
sync.Mutex时,禁止拷贝该结构体(否则锁状态丢失),应导出方法而非暴露字段
最易被忽略的是:在测试或 benchmark 中忘记等待 goroutine 结束,导致读到中间态值——用 sync.WaitGroup 或 chan struct{} 显式同步,而不是靠 time.Sleep。











