http.Client连接复用需正确配置Transport、超时、context及resp.Body.Close,否则形同虚设;包级单例、合理设置MaxIdleConnsPerHost(25–30)、分层超时、匹配IdleConnTimeout与服务端Keep-Alive是关键。

http.Client 默认支持连接复用,但**不配置、不复用、不关 resp.Body,就等于没用**。生产环境里大量 net/http: request canceled (Client.Timeout exceeded while awaiting headers) 或 too many open files 报错,基本都源于这几个动作没做对。
别在 handler 里 new http.Client
每次 &http.Client{} 都会生成独立的 http.Transport,空闲连接彼此隔离,完全无法复用。
- 错误写法:
func handler(w http.ResponseWriter, r *http.Request) { client := &http.Client{}; client.Do(req) } - 正确做法:声明为包级变量或通过依赖注入提供单例,例如
var httpClient = &http.Client{Transport: customTransport} - 若需不同超时策略(如支付调用要 5s,日志上报可 2s),复用同一个
http.Transport,仅改http.Client.Timeout或用context.WithTimeout
MaxIdleConnsPerHost 设太小会卡死请求
它控制「每个目标 host(如 api.pay.example.com:443)最多缓存几个空闲连接」,不是并发上限,也不是活跃连接数限制。默认值是 2,高并发打同一服务时极易触发超时或重建连接。
- 压测稳定 60 QPS 打单个支付网关 → 建议设为
25–30 - 若同时调用 50+ 不同域名,
MaxIdleConns(全局总数)也得同步放大,否则几个热门 host 把池子占满,其他请求只能新建连接或阻塞 - 设太大(如 >100)反而有害:失效连接滞留更久;下游 LB 切 IP 后,旧连接还在池里持续失败
必须分层设超时,且每次请求都要 Close()
Client.Timeout 是兜底总超时,它不中断 DNS 查询、TLS 握手、重定向跳转 —— 这些阶段卡住,goroutine 就永远挂起。
- 必须显式配置:
DialTimeout(建连)、TLSHandshakeTimeout(HTTPS 必设)、ResponseHeaderTimeout(建议 10s) - 每次请求必须用
context.WithTimeout包裹:req, _ := http.NewRequestWithContext(ctx, ...),这是唯一能中断 DNS 和重定向的方式 -
resp.Body.Close()不只是防内存泄漏:不调它,连接永远不会归还到空闲池,复用率直接归零
IdleConnTimeout 和 Keep-Alive 要匹配
这个值决定空闲连接在池里待多久。设太短(如 5s),刚缓存就过期;设太长(如 5m),服务端已断开连接却还留在池里,下次复用直接失败。
立即学习“go语言免费学习笔记(深入)”;
- 推荐 30–90 秒,和下游服务的
Keep-Alive: timeout=60设置对齐 - 确认下游是否真支持 keep-alive:有些 Nginx 配置了
keepalive_timeout 0,等于主动拒绝复用 -
DisableKeepAlives必须为false(默认值),设成true就彻底关掉复用能力


















