Gin 本身不泄漏内存,但 context.WithValue 和 c.Set 易成内存钉子,因其强引用旧 context 及绑定的 *http.Request(含 body、form 等),若存入大对象、未关闭资源或传入长期 goroutine 且未 cancel,会阻断 GC。

Gin 本身不泄漏内存,但用法稍有不慎,就会让对象长期钉在堆上——最常见的是 context.WithValue 持有大对象、goroutine 卡死、全局 map 不清理。
为什么 c.WithValue 和 c.Set 容易变成内存钉子
因为 context.WithValue 返回的新 context 会强引用旧 context,而旧 context 又绑定着 *http.Request(含 body、form data、header 等)。一旦你往里面塞了大结构体、未关闭的 io.Reader、或整个数据库连接,又没控制好生命周期,GC 就永远收不走。
- 错误示例:
c.Set("payload", json.RawMessage(bigJSON))—— 这个bigJSON会随 context 一起被 goroutine 持有,哪怕请求早已结束 - 更隐蔽的坑:
c.Copy()后传进异步 goroutine,但没配context.WithTimeout,副本里的 request body 仍被引用 - 字符串 key 冲突风险:用
c.Set("user", u)和c.Get("user"),若不同中间件都用相同字符串 key,可能覆盖或误读
安全传参的三原则:轻量、专用、限时
不是不能存数据,而是要克制且可控。
- 只存基本类型或小指针:
int64、string、*User(确保*User本身不持有大 buffer 或未关闭资源) - 用自定义类型做 key,避免字符串冲突:
type ctxUserIDKey struct{},然后c.Set(ctxUserIDKey{}, userID) - 异步任务必须加超时:
ctx := context.WithTimeout(c.Request.Context(), 5*time.Second),而不是直接传c.Request.Context() - 绝对不要把
c.Request.Body、c.Request.MultipartForm这类资源型对象塞进 context;需要时,提前读完、解码、释放
排查泄漏必须抓对快照,否则全是噪声
pprof 不是点开就灵,采样方式错了,看到的都是 GC 前的临时对象,根本定位不到真凶。
- 禁用
?gc=1:它强制 GC 后采样,会把泄漏对象“刷掉”,反而看不到钉住的对象 - 文件名必须带后缀:
wget http://localhost:8080/debug/pprof/heap -O before.heap,否则go tool pprof无法识别 - 两次采样间隔 ≥30 秒,且服务处于稳定负载(刚启动或高内存压力时采样无效)
- 关键命令:
go tool pprof -http=:8080 binary before.heap after.heap→ 点 View → Difference → Focus*gin.Context或[]byte,看谁在持续 new 却不释放
goroutine 泄漏比对象泄漏更难察觉,但危害更大
一个卡在 select 或 chan recv 的 goroutine,能拖住整个请求链路上的所有对象——包括你塞进 context 的 hugeStruct、读出来的 []byte、甚至整个 *http.Request。
- 检查入口:
curl "http://localhost:8080/debug/pprof/goroutine?debug=2",搜索大量状态为select、chan receive且 stack 含context或http的 goroutine - 高频泄漏点:忘记
cancel()的context.WithCancel、未 close 的 channel、用time.Ticker但没调ticker.Stop() - 中间件里启 goroutine 时,务必用
c.Copy()+context.WithTimeout,并确保 defer cancel
真正卡住内存的,往往不是某一行 new,而是某个 goroutine 忘了退出,或者某个 map 忘了删键——这些细节在压测初期看不出,跑三天后才爆发。盯住 runtime.MemStats.HeapInuse 的趋势线,比看瞬时值重要得多。











