应复用 http.Client 实例并配置合理的 http.Transport 参数,及时读取并关闭 resp.Body,分层设置超时以避免连接池失效、goroutine 阻塞和 QPS 下降。

复用 http.Client 实例而不是每次新建
Go 的 http.Client 是线程安全且设计为长期复用的。每次请求都 &http.Client{} 会丢失连接池、导致 TCP 握手和 TLS 协商重复发生,显著拖慢吞吐量。
常见错误现象:QPS 上不去、net/http: request canceled (Client.Timeout exceeded while awaiting headers) 频发、大量 CLOSE_WAIT 连接堆积。
- 全局声明一个 client(如
var httpClient = &http.Client{Timeout: 10 * time.Second}),在 HTTP handler 或业务逻辑中直接复用 - 避免在循环或高频调用路径里 new client;哪怕只设一次
Timeout,也比不设强——默认无超时,容易卡死 goroutine - 如需定制 Transport(比如设置代理、跳过证书校验),务必复用同一个
http.Transport实例,否则连接池失效
配置 http.Transport 的连接池参数
默认的 http.Transport 对高并发场景不够友好:最大空闲连接数低、空闲连接存活时间短、不复用 HTTPS 连接等,都会造成频繁建连开销。
典型使用场景:微服务间调用、批量拉取第三方 API、爬虫类任务。
立即学习“go语言免费学习笔记(深入)”;
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
// 可选:禁用 HTTP/2(某些老旧服务不兼容)
// ForceAttemptHTTP2: false,
}
-
MaxIdleConns控制整个 client 的总空闲连接上限;MaxIdleConnsPerHost控制单域名上限,二者需同时调大 -
IdleConnTimeout太小(如默认 30s)会导致连接刚建好就断,太大会占用资源;30–90s 是较稳妥区间 - 若目标服务支持 HTTP/2 且你未显式禁用,Go 默认启用;但某些中间件(如旧版 Nginx、部分 CDN)可能表现异常,此时加
ForceAttemptHTTP2: false可回退到 HTTP/1.1
避免阻塞式读取响应体 + 及时关闭 Response.Body
不读完或不关闭 resp.Body 会让底层连接无法归还给连接池,最终耗尽 MaxIdleConns,后续请求排队甚至超时。
常见错误写法:if resp.StatusCode == 200 { /* 忘了读 Body */ },或仅 defer resp.Body.Close() 却没读内容。
- 始终用
io.ReadAll(resp.Body)或带限长的io.LimitReader读取,即使你只关心状态码 - 如果响应体很大且你只需部分字段,用
json.NewDecoder(resp.Body).Decode(&v)流式解析,它内部会自动读完 - 必须在读完后调用
resp.Body.Close();用defer容易在 error 分支漏掉,建议统一放在函数末尾或用if resp != nil && resp.Body != nil { defer resp.Body.Close() }
按需设置超时:不要只靠 Client.Timeout
Client.Timeout 是“从 RoundTrip 开始到响应头返回”的总时限,它无法覆盖 DNS 解析慢、TLS 握手卡住、响应体传输缓慢等细分阶段。线上常因某环节卡顿导致整体超时不可控。
更精细的做法是分层设超时:
client := &http.Client{
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 5 * time.Second, // TCP 建连
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 5 * time.Second, // TLS 握手
},
Timeout: 10 * time.Second, // 总超时(含读响应体)
}
- DNS 解析由 Go runtime 管理,不直接受
DialContext控制;如需控制,得换用net.Resolver自定义 - 若后端响应体极大(如文件下载),
Client.Timeout会把整个传输过程包进去;此时应单独用time.AfterFunc或 context 控制读取阶段 - 对关键链路,建议用
context.WithTimeout传入client.Do(req.WithContext(ctx)),比全局Timeout更灵活
真正影响性能的往往不是代码写法多炫,而是 Transport 连接池是否生效、Body 是否被读完、超时是否分层覆盖。这三个点漏掉任意一个,压测时 QPS 就会断崖下跌。
