必须用 c.Copy() 启动 goroutine,因为 *gin.Context 被 sync.Pool 复用,原始 c 在 goroutine 执行时可能已被其他请求覆盖,导致数据竞态、路径错乱、panic 等问题。

中间件里直接起 goroutine 用原始 *gin.Context,几乎必然触发数据竞态
为什么 c.Copy() 不是可选项,而是强制要求
Gin 的 *gin.Context 是复用的:一次 HTTP 请求结束后,该结构体被重置并放回 sync.Pool,下次请求可能复用同一内存地址。中间件中启动的 goroutine 如果还拿着原始 c,等它真正执行时,c 早已被其他请求覆盖或重置。
- 典型现象:
c.Get("user_id")返回空、c.Request.URL.Path变成上一个请求的路径、c.MustGet()panic - 竞态检测器会报:
Read at 0x... by goroutine X和Write at 0x... by main goroutine—— 地址相同,但读写发生在不同请求生命周期 - 不是“偶尔出错”,而是只要并发压测,必现;本地单请求跑一百次都正常,一上压测就崩
go func() { ... }() 捕获循环变量导致的隐式共享
在 Gin 中批量注册中间件或遍历路由时,如果写成 for _, mw := range middlewares { r.Use(mw) } 并在内部起 goroutine,且闭包里直接用了 mw 或循环索引,就会复用同一个变量地址。
- 错误写法:
for i, name := range names { r.GET("/"+name, func(c *gin.Context) { go func() { log.Println(i, name) }() }) }—— 所有 goroutine 输出的都是最后一个i和name - 根本原因:闭包捕获的是
i和name的内存地址,不是值;而 for 循环只分配一份栈空间给它们 - 修复只需加一层传参:
go func(n string, idx int) { log.Println(idx, n) }(name, i),或提前声明副本:nm := name; idx := i; go func() { ... }()
中间件里用 sync.Mutex 保护共享状态反而更危险
中间件本身是串行执行的(按注册顺序),但一旦你在中间件里启了 goroutine 去修改全局计数器、缓存 map 或配置字段,就立刻变成多 goroutine 并发访问——此时加锁位置稍有偏差,就会失效。
- 常见错误:只在中间件主流程加锁,但 goroutine 里直接读写
sharedMap,没包进锁区 - 更隐蔽的坑:用
sync.RWMutex,读操作加RLock(),但 goroutine 里写了sharedMap[key] = val却忘了用Lock() - 推荐替代方案:对简单计数器优先用
atomic.AddInt64(&counter, 1);对 map 类型,改用sync.Map或把读写逻辑收口到单个 goroutine + channel
最易被忽略的一点:Gin 中间件的生命周期和请求绑定,但 goroutine 的生命周期不绑定。哪怕你加了 ctx.Done() 监听,也拦不住已经启动、正在读原始 c 的 goroutine —— 它拿到的是过期上下文,且无法被取消。所以不是“加了 context 就安全”,而是“必须用 c.Copy() 启动 goroutine”这个动作本身,就切断了与原始上下文的内存关联。


















