int类型计数器在goroutine中出错是因为i++非原子操作,涉及读-改-写三步,多goroutine并发时会相互覆盖;解决方法只有加锁(sync.Mutex)或原子操作(sync/atomic)。

为什么 int 类型的计数器在 goroutine 中会出错
多个 goroutine 同时对一个普通 int 变量做 ++ 或 --,结果大概率小于预期。这不是“偶尔出错”,而是必然发生——因为 i++ 实际包含读取、加 1、写回三步,中间可能被其他 goroutine 插入执行,导致覆盖式写入。比如两个 goroutine 同时读到值 5,各自加 1 后都写回 6,最终只 +1 而非 +2。
常见错误现象:counter 最终值远低于启动的 goroutine 数量;每次运行结果不一致;加了 time.Sleep 后“好像变正常”(其实是掩盖而非修复)。
解决思路只有两种:加锁(sync.Mutex)或原子操作(sync/atomic)。前者通用但有开销,后者轻量但仅支持基础类型和有限操作。
sync/atomic 能做什么、不能做什么
sync/atomic 提供的是底层 CPU 级原子指令封装,适用于 int32、int64、uint32、uint64、uintptr 和指针类型。它不支持 float64 或结构体,也不能直接实现“先读再条件写”这类复合逻辑(那得用 CompareAndSwap 手动重试)。
立即学习“go语言免费学习笔记(深入)”;
常用函数包括:atomic.AddInt64、atomic.LoadInt64、atomic.StoreInt64、atomic.CompareAndSwapInt64。注意参数顺序固定:第一个是地址(*int64),第二个是操作数(如增量值)。
示例(安全递增):
var counter int64
go func() {
atomic.AddInt64(&counter, 1)
}()⚠️ 容易踩的坑:
- 传值而非取地址(atomic.AddInt64(counter, 1) 编译不通过)
- 混用 int 和 int64(32 位系统上 int 是 32 位,atomic 对 int 无直接支持)
- 忘记初始化(未显式初始化的包级 int64 默认为 0,但局部变量必须初始化)
什么时候该用 sync.Mutex 而不是 atomic
当计数逻辑超出原子操作能力范围时,就必须切到锁。典型场景包括:
- 需要同时更新多个关联变量(如计数器 + 时间戳 + 状态标志)
- 要实现“如果当前值小于 X 则加 1”这类带条件的更新
- 需支持浮点数累加(
atomic不提供float64原子加法) - 计数器要嵌套在结构体中,且结构体其他字段也需并发保护
示例(带条件的计数):
type SafeCounter struct {
mu sync.Mutex
n int64
}
func (c *SafeCounter) IncIfLessThan(threshold int64) bool {
c.mu.Lock()
defer c.mu.Unlock()
if c.n < threshold {
c.n++
return true
}
return false
}性能提示:单纯计数场景下,atomic 比 Mutex 快 5–10 倍;但一旦涉及复杂判断或多字段协调,锁更清晰可靠,别硬套原子操作。
真实项目中容易被忽略的边界点
实际用起来,这几个细节常被跳过却直接影响正确性:
-
atomic.LoadUint64和atomic.LoadInt64返回的是副本,不是引用——不能拿去取地址再传给其他原子函数 - 跨平台注意:32 位系统上
atomic.AddInt64要求 64 位对齐,结构体中若前面是int32,后面跟int64字段,可能触发 panic;用//go:align 8或调整字段顺序可规避 - 测试并发问题不能只靠“跑一次”,要用
-race检测数据竞争:go run -race main.go - 日志打印计数器值时,务必用
atomic.LoadInt64(&counter),而不是直接读变量——否则可能看到撕裂值(尤其int64在 32 位系统上非原子读)
最麻烦的其实不是写对,而是改错:已有代码用 int 计数,上线后才发现不准,此时不能只改类型为 int64 就完事,所有读写点都得统一换成 atomic 函数调用,漏一处就前功尽弃。










