Go中安全计数器应优先使用sync/atomic包的原子操作(如atomic.AddInt64),而非互斥锁;需用int64类型并确保字段自然对齐,所有读写必须统一使用atomic函数,避免竞态。

在 Go 中实现安全计数器,核心是避免竞态(race),关键手段是使用 sync/atomic 包提供的原子操作,而不是依赖互斥锁(sync.Mutex)——除非你需要更复杂的同步逻辑。
为什么原子操作更适合简单计数器
计数器本质是单个整型变量的增减读取。atomic 对 int32、int64、uint32、uint64、uintptr 和指针类型提供了无锁、CPU 级别的原子读写与修改,性能高、开销小、天然并发安全。
注意:必须用 int64(而非 int)配合 atomic,因为 int 在 32 位系统上是 32 位,无法保证 64 位原子性;Go 官方只对明确位宽的类型提供原子支持。
基础安全计数器实现
以下是一个线程安全、零依赖的计数器结构:
立即学习“go语言免费学习笔记(深入)”;
type Counter struct {
val int64
}
func (c *Counter) Add(delta int64) {
atomic.AddInt64(&c.val, delta)
}
func (c *Counter) Inc() {
atomic.AddInt64(&c.val, 1)
}
func (c *Counter) Dec() {
atomic.AddInt64(&c.val, -1)
}
func (c *Counter) Load() int64 {
return atomic.LoadInt64(&c.val)
}
func (c *Counter) Store(v int64) {
atomic.StoreInt64(&c.val, v)
}
func (c *Counter) Swap(new int64) (old int64) {
return atomic.SwapInt64(&c.val, new)
}
func (c *Counter) CompareAndSwap(old, new int64) bool {
return atomic.CompareAndSwapInt64(&c.val, old, new)
}
所有方法都无需加锁,可被任意 goroutine 并发调用。例如:
c := &Counter{}
go c.Inc()
go c.Inc()
go c.Add(3)
// 最终 c.Load() == 5,无竞态
常见陷阱与注意事项
-
不要对非对齐字段做原子操作:结构体中
int64字段必须自然对齐(通常放在结构体开头或确保前面字段总大小是 8 的倍数),否则在某些架构(如 32 位 ARM)上 atomic 可能 panic。Go 1.19+ 已大幅缓解,但稳妥起见仍建议把int64放首位。 -
避免混用 atomic 和普通读写:一旦用了
atomic.LoadInt64,所有读写都必须用 atomic 函数。直接c.val++会破坏原子性。 -
复合操作需谨慎:比如“如果为 0 则设为 1”,这不是单原子操作。此时应使用
CompareAndSwap循环重试,或改用sync.Mutex保护临界区。 -
不支持 float64 原子运算:需要浮点计数?要么转为定点整数(如乘以 100 存为 int64),要么用互斥锁 +
math.Float64bits/math.Float64frombits手动封装(极少必要)。
何时该换用 Mutex
当你的“计数器”行为超出原子整数能力时,例如:
- 需要同时更新多个字段(如计数器 + 时间戳 + 状态标志)
- 要实现带限流逻辑的计数(如“每秒最多 100 次,超限返回错误”)
- 需阻塞等待(如 wait-until-zero)
- 要支持复杂查询(如 TopN、滑动窗口统计)
这时用 sync.Mutex 封装更清晰可靠,性能差异在绝大多数场景下可忽略。
基本上就这些。用 atomic 实现计数器不复杂但容易忽略对齐和类型细节,抓住 int64 + atomic.*Int64 这个组合,就能写出高效又安全的并发计数逻辑。










