pprof -inuse_space 显示 http.request 或 model.user 却无分配点,说明被 handler 中 defer 或 goroutine 闭包隐式捕获;需聚焦可疑 handler,检查是否直接引用 req、body 等长生命周期对象,应显式传参而非闭包捕获。

pprof -inuse_space 看到 *http.Request 或 *model.User 却找不到分配点?查 handler 闭包
这几乎可以断定是闭包隐式捕获了请求上下文或业务对象。pprof 显示 *model.User 占用高,但调用栈只到 http.HandlerFunc.ServeHTTP → runtime.deferproc,说明它不是 new 出来的,而是被某个 defer 或 goroutine 闭包“拽住”了。
- 打开 pprof Web UI:
go tool pprof -http=:8080 binary heap.pb.gz,对可疑类型点「Focus」,再选「View → Call graph」 - 如果图中大量分支收束到同一个 handler 函数名,且末端是
defer func()或go func(),就是它 - 重点检查该 handler 内所有匿名函数:是否直接用了
req、resp、rows *sql.Rows、body []byte等长生命周期对象
defer func() { log.Println(req) }() 是高危写法,必须显式传参
这种写法会让闭包隐式捕获整个 req *http.Request,包括它的 Body 底层数组、Header map、TLS 连接等——哪怕你只打印 req.URL.Path,整块内存都钉在堆上,直到 handler 函数返回。
- 错误:
defer func() { log.Printf("path: %s", req.URL.Path) }()→req被完整捕获 - 正确:
defer func(path string) { log.Printf("path: %s", path) }(req.URL.Path)→ 只传需要的字段 - 若需 body 内容,先读取再传小结构体:
b, _ := io.ReadAll(req.Body); defer func(b []byte) { /* ... */ }(b)
for 循环里闭包捕获 i,不只是逻辑错,更是内存钉子
Go 的 for 循环变量 i 是复用的,所有闭包共享同一地址。现象是:闭包列表执行时全输出最终值(比如全是 10),更严重的是,这个 i 所在栈帧无法回收,连带它引用的其他大对象也被锁死。
- 错误:
for i := 0; i - 修复必须在循环体内做副本:
for i := 0; i - 注意:
go func(i int) { ... }(i)对立即启动的 goroutine 有效,但若闭包要存起来延后调用(比如注册进 map),仍需i := i方式
goroutine 启动时闭包捕获 req,又没监听 ctx.Done(),等于造孤儿
在 handler 里写 defer go func() { ... }() 是典型陷阱。goroutine 启动后脱离请求生命周期,若没绑定上下文,就会变成长期存活的“孤儿”,持续持有 req、resp、buffer 等。
- 错误:
defer go func() { log.Println(req.URL.Path) }()→ 没ctx.Done()监听,也没资源清理 - 正确:
go func(ctx context.Context) { select { case - 更稳妥用
context.WithTimeout+sync.WaitGroup控制生命周期,避免裸go func()
真正麻烦的不是闭包本身,而是它捕获的“短命对象”——比如 handler 里一个临时 user *model.User,被闭包塞进全局事件总线,没人注销,它就永远活在堆上。











