慢响应会卡住goroutine,因其常因未设超时、未关resp.Body、未传有效context,导致子goroutine永久阻塞在HTTP调用或通道操作上,无法退出并持续占用栈内存、文件描述符等资源。

接口响应过慢本身不直接导致资源泄露,但它是 goroutine 和 http.Response.Body 泄露的典型诱因——只要没设超时、没关 body、没传 context,慢响应就会把 goroutine 卡死、把连接池耗尽、把内存越堆越高。
为什么慢响应会卡住 goroutine
HTTP handler 里启动子 goroutine 但没绑定 req.Context(),或用了 context.Background(),一旦下游响应延迟,这个 goroutine 就永远等在 、<code>http.Do() 或 time.Sleep() 上,状态变成 chan receive 或 select,且阻塞时长持续增长(比如看到 432000s)。
- 所有衍生 goroutine 必须监听
ctx.Done(),且select中第一个case就该是它 - 避免裸写
go func() { ... }(),必须带ctx参数并透传 - 用
http.NewRequestWithContext(ctx, ...)替代http.NewRequest(...)
Response.Body 不 close 是最隐蔽的泄漏点
resp.Body.Close() 不只是释放网络连接——它直接影响底层 persistConn 的复用和回收。漏掉这句,连接就卡在 idle 状态,对应 goroutine 堆在 net/http.readLoop 里,数量随请求线性上涨。
- 必须在
defer resp.Body.Close()前检查err != nil,否则 panic 会导致 defer 不执行 - 不要用
io.ReadAll(resp.Body)后再 close——它内部已读完,但 close 仍必需 - 如果要用
io.Copy流式转发,记得用io.MultiReader或显式 close 原始 body
pprof /debug/pprof/goroutineleak 端点怎么用才有效
Go 1.24+ 支持原生 /debug/pprof/goroutineleak,但它不是“开箱即用”,得满足两个硬条件:环境变量启用 + GC 完成一次感知循环。
- 启动加
GODEBUG=goleak=1(注意不是gctrace=1,那个只打日志) - 首次访问
/debug/pprof/goroutineleak?debug=1前,手动调一次runtime.GC() - 返回结果中带
_Gleaked标记的 goroutine 才是确认泄漏项,普通chan receive不算 - 该端点对死循环、无限重试无效,只抓“阻塞在不可达同步原语上”的 goroutine
监控指标比 pprof 更早发现问题
靠人工查 pprof 是救火,靠指标监控才是防守。真正容易被忽略的是:泄漏发生前,runtime.NumGoroutine() 和 http.Server.IdleConns 已经开始缓慢爬升,而日志里没有任何 error。
- 在 HTTP middleware 里记录
runtime.NumGoroutine()进出值,差值 > 5 就告警 - 暴露
/metrics端点,采集http_server_open_connections和http_client_idle_connections - 用 Prometheus 抓取
process_resident_memory_bytes+go_goroutines,设同比增幅 > 30% 持续 5 分钟触发告警
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











