不能直接用普通变量做并发计数器,因为counter++实际拆分为“读取、+1、写回”三步,非原子操作会导致数据竞争、计数值偏小;应使用sync/atomic.AddInt64等原子操作,且变量须为int64类型并传入指针地址。

为什么不能直接用普通变量做并发计数器
因为 int 类型的自增(比如 counter++)不是原子操作——它实际拆成“读取、+1、写回”三步。在 Gin 的 HTTP handler 里,多个请求同时进来时,会竞争修改同一个变量,结果就是丢数据。你发 1000 个并发请求,最后 counter 可能只加了 800 多次。
用 sync/atomic 替代锁是最简方案
Go 标准库的 sync/atomic 提供无锁原子操作,比 sync.Mutex 更轻量,也更适合只做计数这种简单场景。注意:必须用指针传入,且类型严格限定为 int32 或 int64(int 在 32 位系统上不安全)。
实操建议:
- 定义全局变量时用
int64,比如:var counter int64 - 读取用
atomic.LoadInt64(&counter),别直接读counter - 自增用
atomic.AddInt64(&counter, 1),返回新值;需要旧值就先读再加 - 不要混用普通赋值和原子操作,比如
counter = 0是非原子的,应改用atomic.StoreInt64(&counter, 0)
Gin 路由里怎么安全暴露计数器
把原子变量封装进 handler,避免意外暴露或误操作。常见错误是每次请求都新建一个计数器,或者把 &counter 传进闭包导致逃逸。
立即学习“go语言免费学习笔记(深入)”;
正确写法示例:
func main() {
r := gin.Default()
var counter int64
r.GET("/count", func(c *gin.Context) {
c.JSON(200, gin.H{"count": atomic.LoadInt64(&counter)})
})
r.POST("/count/inc", func(c *gin.Context) {
atomic.AddInt64(&counter, 1)
c.Status(204)
})
r.Run(":8080")
}
关键点:
- 变量声明在 handler 外层,生命周期与 server 一致
- GET 和 POST 共享同一个
&counter地址,确保原子操作作用于同一内存位置 - 不用
sync.Once或 init 函数初始化计数器——普通零值初始化就够了
如果要重置或批量更新怎么办
atomic 没有“加到指定值”或“条件重置”这类高级操作。想实现 reset 接口,只能用 atomic.StoreInt64(&counter, 0);想实现“加 N”,就用 atomic.AddInt64(&counter, n)。
容易踩的坑:
- 传入负数给
AddInt64是合法的,等于减法,但别忘了检查n是否为负——HTTP 参数可能被恶意构造 - 如果需要“仅当当前值为 X 时才更新”,得用
atomic.CompareAndSwapInt64,但 Gin handler 里很少真需要 CAS,多数情况是过度设计 - 别试图用
atomic.Value包装int——它适合存指针或结构体,对基础数值类型反而多一层间接,还慢
真正麻烦的从来不是原子操作本身,而是你忘了所有读写都必须走原子函数——漏掉一次普通赋值,整个并发安全就崩了。


















