Go 的 http.DefaultClient 默认复用连接但常因配置失误、服务端响应头含 Connection: close、host 大小写不一致或未调用 resp.Body.Close() 等导致复用中断;需显式配置 MaxIdleConns、MaxIdleConnsPerHost、IdleConnTimeout 等参数并确保所有请求正确关闭响应体。
go 的 http.defaultclient 默认复用连接,但“默认启用”不等于“默认可靠”——连接泄露几乎全因配置、调用或生命周期管理失误,而非复用机制本身失效。
为什么 http.DefaultClient 复用看起来没生效
现象不是“没复用”,而是复用被悄悄打断。常见干扰点:
-
http.DefaultTransport被其他库修改过(比如某 SDK 设置了MaxIdleConns = 1),你没察觉,但所有走默认 client 的请求都卡住了 - 服务端响应头含
Connection: close(用curl -v可见),客户端只能新建连接 - 请求 URL 的 host 大小写不一致(
https://API.EXAMPLE.COM和https://api.example.com被视为两个 host,各自维护独立连接池) - 手动设置了
req.Close = true或写了req.Header.Set("Connection", "close"),等价于主动弃用 keep-alive
排查连接泄露的三个关键命令
别只看 QPS 或错误日志,直接查系统级资源状态:
-
lsof -i :443 | wc -l:持续上涨且不回落 → fd 在泄漏,不是临时高峰 -
netstat -an | grep ':443' | grep -E '(ESTABLISHED|TIME_WAIT)' | wc -l:若ESTABLISHED持续堆积,说明连接没归还 idle pool;若TIME_WAIT爆涨,大概率是复用被禁用(如DisableKeepAlives = true) -
go tool pprof http://localhost:6060/debug/pprof/goroutine?debug=2:搜索http.(*Transport).roundTrip,若大量 goroutine 卡在select或chan receive,说明连接池阻塞,等待空闲连接
必须显式配置的 http.Transport 参数
默认值只适合本地调试,高并发下不设等于放弃复用控制权:
-
MaxIdleConns:建议设为500(全局最大空闲连接数),避免跨 host 争抢 -
MaxIdleConnsPerHost:必须与MaxIdleConns同值(如500),否则单 host 成为瓶颈 -
IdleConnTimeout:设为60 * time.Second(略小于 Nginx 默认keepalive_timeout 75s),太短导致频繁建连,太长让僵尸连接占着 fd -
TLSHandshakeTimeout和ResponseHeaderTimeout:至少设为10 * time.Second,防 TLS 握手卡死或服务端不发 header 导致连接悬挂
resp.Body.Close() 不是可选项,是释放连接的唯一开关
漏掉它,连接就永远进不了 idle pool,哪怕 client 和 transport 配置完美也没用:
- 错误写法:
defer resp.Body.Close()写在函数开头,但err != nil时resp是nil→ panic - 更隐蔽的错:循环中把
defer写在循环外,只有最后一次请求的 body 被关 - 正确姿势:先判
err,再确保resp非空后立刻Close();若需读取 body,用defer resp.Body.Close()包裹读取逻辑(如io.ReadAll) - 注意:关闭
resp.Body不等于关闭 TCP 连接,只是告诉 Transport “这个连接可以回收了”
真正难排查的不是参数设多少,而是哪一行代码漏掉了 resp.Body.Close(),或者哪个第三方库偷偷改了 http.DefaultTransport。压测时看到 lsof 持续上涨,第一反应不该是调大 MaxIdleConns,而是检查所有 http.Do 调用点是否都配对了 Close()。


















