用 atomic.addint64 即可实现轻量并发安全计数,但仅适用于单操作原子性;涉及“先读后改”等复合逻辑时必须换 sync.mutex,多 key 统计需配锁或分片 map。

用 atomic.AddInt64 就够了,别碰 counter++
Go 里最轻量、最常用的并发安全计数器,就是靠 sync/atomic 包里的原子操作。它不加锁、无调度开销,CPU 指令级保证单次读写或增减的完整性。
常见错误现象:var counter int 然后多个 goroutine 同时执行 counter++,最终值远小于预期(比如 1000 个 goroutine 各加 1,结果只有 200),go run -race 会立刻报 Data race。
- 必须用
int64类型(不是int),否则在 32 位系统或某些架构上可能 panic - 所有读写都得走原子函数:增用
atomic.AddInt64,读用atomic.LoadInt64,写用atomic.StoreInt64,混用普通赋值(如c.val = 0)会导致未初始化读取到垃圾值 - 结构体中
int64字段要自然对齐——稳妥起见,把它放在结构体第一个字段 -
atomic.AddInt64返回新值;传负数(如-1)可实现减法,但业务上是否允许减、是否需防负,得由上层逻辑控制,原子包不负责校验
type Counter struct {
val int64
}
func (c *Counter) Inc() { atomic.AddInt64(&c.val, 1) }
func (c *Counter) Get() int64 { return atomic.LoadInt64(&c.val) }
什么时候必须换 sync.Mutex?
一旦操作里出现“先读再决定怎么改”,atomic 就不够用了——它只保单条指令原子性,不保逻辑原子性。
使用场景举例:限流计数器(超过阈值就拒绝)、状态机切换(仅当当前为 Running 才能设为 Stopping)、或需要在增减前后做日志/通知等副作用。
-
if counter > 100 { atomic.AddInt64(&counter, -1) }是典型错误:两次原子调用之间,其他 goroutine 可能已把值改掉 - 这种复合判断 + 修改,要么用
atomic.CompareAndSwapInt64加循环重试(适合冲突低、逻辑极简),要么直接上sync.Mutex -
sync.Mutex开销在百万级/秒计数下才明显,日常 HTTP 请求计数、错误统计等完全扛得住,且代码更直白、易维护
要统计不同 key 的次数,别裸用 map
多个 goroutine 同时往一个 map[string]int64 写,运行时必 panic:fatal error: concurrent map writes。这不是概率问题,是 Go 主动中止程序防止数据损坏。
实操建议优先选 sync.RWMutex + 普通 map:
- 读多写少(比如每秒查 1000 次状态,只写 10 次)时,
RWMutex允许多读并发,比普通Mutex更高效 -
sync.Map不是银弹:它不支持range,遍历必须用Range()回调,值类型是interface{},还得类型断言;键频繁增删或需全量快照时,反而不如带锁的普通 map 清晰 - 如果 key 数量极大(千万级)、写压力均匀,可考虑分片 map(sharded map),但多数业务用不到
type KeyCounter struct {
mu sync.RWMutex
data map[string]int64
}
func (c *KeyCounter) Inc(key string) {
c.mu.Lock()
c.data[key]++
c.mu.Unlock()
}
func (c *KeyCounter) Get(key string) int64 {
c.mu.RLock()
defer c.mu.RUnlock()
return c.data[key]
}
高频调用时三个容易被忽略的细节
哪怕你全用对了 atomic,高并发下仍可能出问题——不是功能错,而是行为不可控或性能反降。
- 初始化必须用
atomic.StoreInt64(&counter, 0),不能直接counter = 0;跨包导出变量尤其危险,其他 goroutine 可能读到未写入完成的中间态 - 别在 for 循环里反复
atomic.LoadInt64判断条件再AddInt64,这本质是自旋,浪费 CPU,还可能饿死其他 goroutine;该用sync.WaitGroup或 channel 控制节奏就别硬刚原子操作 - 哈希类计数器(如 Counting Bloom Filter)里,别复用同一个
hash.Hash实例;fnv.New64()每次都要新建,否则多个 goroutine 并发Write()会导致哈希值错乱——这是静默错误,很难排查
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











