Gin中全局变量并发不安全,count++因非原子性会丢数据;应使用sync/atomic保护整数、sync.Mutex保护复杂状态,并避免在goroutine中误用c.Request.Context()。

Gin 本身不提供协程安全的全局状态管理,所有共享变量(如计数器、缓存、DB 实例)在并发请求下都可能出错;真正的并发安全必须由开发者显式保障。
为什么 count++ 在 Gin 中会丢数据
Go 的 count++ 不是原子操作,它拆成「读值 → 加 1 → 写回」三步。当多个 goroutine 同时执行该语句,彼此可能读到同一个旧值,最终只加了一次。用 ab -n 10000 -c 100 压测时,count 往往远小于 10000,就是典型的数据竞争表现。
常见错误模式:
- 在 handler 外定义
var count int,handler 内直接count++ - 用
map[string]interface{}存全局状态,多 goroutine 并发读写未加锁 - 把
*sql.DB或*redis.Client当作普通变量反复赋值或重置
用 sync/atomic 保护简单整数
对计数器、开关标志这类单一整型变量,sync/atomic 是最轻量、零锁开销的方案,但要求类型为 int64、uint64 或指针。
正确写法:
var count int64 = 0
r.GET("/counter", func(c *gin.Context) {
newVal := atomic.AddInt64(&count, 1)
c.JSON(200, gin.H{"count": newVal})
})
注意点:
- 不能对
int直接用atomic.AddInt64,必须声明为int64 -
atomic.LoadInt64(&count)才能安全读取当前值,别用count直接读 - 不适用于浮点数或结构体字段更新
用 sync.Mutex 保护复杂逻辑或多个变量
当需要原子地更新多个字段、执行条件判断再写入、或涉及非整型数据时,sync.Mutex 是通用解法。
示例:
type Stats struct {
Total int64
Success int64
Lock sync.Mutex
}
var stats Stats
r.POST("/upload", func(c *gin.Context) {
stats.Lock.Lock()
defer stats.Lock.Unlock()
stats.Total++
if isOK {
stats.Success++
}
c.JSON(200, gin.H{"total": stats.Total})
})
关键细节:
- 锁粒度要小:只包裹真正共享访问的代码段,别把
c.JSON或日志也锁进去 - 务必
defer Unlock(),否则极易死锁 - 不要把
Mutex放进gin.Context.Set()或跨 goroutine 传递
异步任务中误用 c.Request.Context()
这是 Gin 高并发中最隐蔽的坑:在 go func() { ... }() 里直接使用 c.Request.Context(),请求一结束 context 就被 cancel,后续 DB 查询或 HTTP 调用立刻失败,报 context canceled。
正确做法只有两个:
- 用
c.Copy()复制一个脱离请求生命周期的新上下文:ctx := c.Copy(),再传给 goroutine - 改用独立的、带超时控制的后台 context:
ctx, _ := context.WithTimeout(context.Background(), 30*time.Second)
绝对禁止:
go func() { db.WithContext(c.Request.Context()).First(...) }()- 把
*gin.Context整个传进 goroutine —— 它不是线程安全的,且生命周期绑定请求
真正难的不是加锁或用原子操作,而是识别哪些变量/资源是“共享”的:全局变量、单例 DB 连接、缓存 map、甚至日志输出缓冲区——只要多个 goroutine 可能同时触碰,就必须做并发控制。别依赖“好像没出过问题”来赌运气。


















