http client未复用、transport配置不当及未关闭resp.body是导致连接数暴涨的三大主因,需全局复用client、显式配置maxidleconns等参数,并始终defer resp.body.close()。

HTTP Client没复用导致连接数暴涨
Go 的 http.Client 默认会复用连接,但前提是你要**复用同一个 client 实例**。很多人在 handler 里每次请求都 new 一个 http.Client,看起来没问题,实际每秒几百请求就能把 netstat -an | grep :80 | wc -l 拉到上万,且大量处于 TIME_WAIT 状态。
常见错误写法:
func handler(w http.ResponseWriter, r *http.Request) {
// ❌ 错:每次请求都新建 client
client := &http.Client{}
resp, _ := client.Get("https://api.example.com")
// ...
}- 正确做法是定义全局或包级变量复用
http.Client,比如var httpClient = &http.Client{Timeout: 30 * time.Second} - 如果需要不同超时或 Transport 配置,也应按用途分组复用,而不是按请求新建
- 注意:
http.DefaultClient可以直接用,但它共享了http.DefaultTransport,一旦被其他库修改(比如某 SDK 调了http.DefaultTransport.(*http.Transport).MaxIdleConns = 1),会影响全局限流行为
Transport 配置不当引发连接堆积
http.Transport 是连接复用的实际控制者,不显式配置的话,Go 1.12+ 默认的 MaxIdleConns 和 MaxIdleConnsPerHost 都是 100,看似够用,但在高并发短连接场景下容易卡住。
典型现象:压测时 QPS 上不去,curl -v 明显卡在 CONNECT 阶段,lsof -i :443 | wc -l 持续增长但不释放。
立即学习“go语言免费学习笔记(深入)”;
- 必须显式设置
MaxIdleConns和MaxIdleConnsPerHost,建议至少设为500或更高(视后端机器数和连接能力而定) -
IdleConnTimeout建议设为30s,太长会让空闲连接占着端口不放;太短(如5s)会导致频繁建连,抵消复用收益 - 别漏掉
TLSHandshakeTimeout和ResponseHeaderTimeout,否则 TLS 握手卡住或服务端不发 header 时,连接会一直挂着
忘记关闭 resp.Body 导致连接无法释放
这是最隐蔽也最常踩的坑:即使用了正确的 http.Client 和 Transport,只要没调 resp.Body.Close(),底层连接就不会归还到 idle pool,下次请求就只能新建连接。
错误示例:
resp, err := client.Get(url)
if err != nil { return }
// ❌ 忘了 defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
// ... 后续没关 body- 必须在读完 body 后立刻
defer resp.Body.Close(),哪怕你只读前几个字节或直接丢弃 - 如果用
io.Copy或json.NewDecoder(resp.Body),同样要 close;Go 不会自动帮你关 - 注意:如果 resp.StatusCode >= 400,很多人会跳过读 body,但依然得
resp.Body.Close(),否则连接卡死
pprof + net/http/pprof 查不到连接泄露?
连接池泄漏本身不会体现在 goroutine 或 heap pprof 里,它藏在 http.Transport 的内部字段中,得靠更底层的指标。
真正有效的排查路径:
- 先看系统连接数:
ss -s或netstat -an | awk '$6 ~ /ESTABLISHED|TIME_WAIT/ && $4 ~ /:443$/ {++s} END {print s}' - 再查 client 内部状态:给 client 的
Transport加个 wrapper,在关键路径打日志,或用transport.IdleConnMetrics()(Go 1.21+)获取实时 idle 连接数 - 临时加 debug 接口输出 transport 状态:
transport.IdleConnTimeout、transport.MaxIdleConns、当前transport.idleConnmap 长度(需反射或改源码打点) - 别依赖
/debug/pprof/goroutine?debug=2找“谁没关 body”,goroutine 可能早就结束了,body 没关的影响是连接滞留,不是协程堆积
连接池问题从来不是单点故障,而是 client 复用、transport 配置、body 关闭三者配合失效的结果。少一个环节,泄漏就藏得更深一点。










