中间件里直接读写count会出错,因为count++是非原子操作,底层拆为读值→加1→写回三步,多goroutine并发执行时会相互覆盖导致计数丢失;应改用int64类型配合atomic.addint64和atomic.loadint64实现线程安全。

为什么中间件里直接读写 count 会出错
你在中间件或 handler 里定义了一个全局变量 count,然后在 c.GET 中做 count++,压测时发现数值远小于预期——这不是 Gin 的 bug,而是 Go 并发模型的必然结果。count++ 在底层拆成三步:读值 → 加 1 → 写回。多个 goroutine 同时执行这三步,就会发生“写覆盖”,比如两个 goroutine 都读到 100,各自加 1 后都写回 101,最终只 +1 而非 +2。
用 atomic.AddInt64 替代普通自增
对单一整数计数器,sync/atomic 是最轻量、最安全的选择。它依赖 CPU 指令级原子性,无锁、无调度开销。
-
count必须声明为int64类型(atomic对int不保证跨平台原子性) - 自增统一用
atomic.AddInt64(&count, 1),它返回新值,可直接用于响应 - 读取当前值必须用
atomic.LoadInt64(&count),不能直接读变量 - 别混用:一旦用了原子操作,所有读写都得走
atomic系列函数,否则仍不安全
什么时候该换 sync.Mutex
原子操作只适合简单类型(int32/int64/uint32/uint64/uintptr/unsafe.Pointer)。如果你要保护的是结构体字段、map、slice 或需要多步逻辑(比如“检查余额 > 0 后再扣款”),就必须上锁。
- 定义
var mu sync.Mutex和受保护的变量(如var stats map[string]int) - 读写前调
mu.Lock(),结束后立刻mu.Unlock()(推荐 defer) - 避免在锁内做耗时操作(如 HTTP 请求、DB 查询),否则会阻塞其他 goroutine
- 注意死锁:同一个 goroutine 不要重复 Lock,也不要跨函数传递未解锁的 Mutex
c.Copy() 不是并发安全的银弹
很多人以为在中间件里启动 goroutine 时只要调了 c.Copy() 就万事大吉——其实它只解决“上下文被提前回收”问题,不解决“内部字段并发读写冲突”。比如你在 c.Set("user", &User{}) 后又在 goroutine 里修改这个 *User 字段,依然可能数据错乱。
-
c.Copy()本质是深拷贝整个*gin.Context结构,包括Keys(map[string]interface{})、Errors([]error)等——这些本身都不是线程安全的 - 若你真要在 goroutine 里改共享数据,要么用原子操作(仅限基础类型),要么自己加锁,要么把数据转成不可变副本(如只读 struct 字段)
- 更推荐的做法是:goroutine 只读取必要字段(如
c.ClientIP()、c.Request.URL.Path),然后传值进去,彻底规避共享状态











