必须显式调用 resp.body.close(),否则连接卡在 readloop、不进 idle 池也不释放 fd,导致 too many open files、goroutine 堆积、qps 低而 established 连接数飙升;正确做法是 err 检查后立即 defer 或显式关闭,且 transport 需合理配置 maxidleconns 等参数。

为什么 resp.Body.Close() 必须显式调用
Go 的 http.Client 默认启用 keep-alive,但连接能否复用,完全取决于你是否消费并关闭了 resp.Body。不关,连接就卡在 persistConn.readLoop 里,既不进入 idle 池,也不释放 fd。
常见错误现象:
-
socket: too many open files频发,lsof -i :443 | wc -l持续上涨 -
pprof显示大量 goroutine 停在net.(*pollDesc).Wait或http.(*Transport).roundTrip - QPS 很低(比如每秒 5 次),但 ESTABLISHED 连接数却涨到几百
正确做法不是“看情况 defer”,而是:只要拿到 resp,且 err == nil,立刻安排关闭 —— 不管你后续要不要读 body。
示例:
resp, err := client.Get(url)
if err != nil {
return err
}
defer resp.Body.Close() // ✅ 放在 err 检查之后、任何读取之前
// 后续可安全调用 io.ReadAll(resp.Body) 或 json.NewDecoder(resp.Body).Decode(...)
注意:defer 在函数退出时才执行。若该函数生命周期长(如常驻 worker),应改用显式 resp.Body.Close(),避免堆积。
Transport 配置不当会让连接“假释放”
即使你每次都关了 resp.Body,若 http.Transport 参数不合理,连接仍会卡在 idle 状态不释放,或反复新建连接。
关键配置项及建议值:
-
MaxIdleConns和MaxIdleConnsPerHost:默认 100,高并发下极易成为瓶颈;建议统一设为500或更高,需匹配后端实例数 -
IdleConnTimeout:空闲连接存活时间;设太长(如 90s)导致 fd 占着不放,太短(如 5s)引发频繁建连;推荐30 * time.Second -
TLSHandshakeTimeout和ResponseHeaderTimeout:防止 TLS 握手卡死或服务端迟迟不发 header,推荐均设为10 * time.Second
典型错误是直接用 &http.Client{} 或复用 http.DefaultClient,后者可能被第三方 SDK 悄悄修改 Transport 参数,造成全局影响。
安全初始化写法:
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 500,
MaxIdleConnsPerHost: 500,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
},
}
for 循环里 defer resp.Body.Close() 是无效的
defer 绑定的是当前函数退出时机,不是每次循环迭代结束。把它写在 for 外部,只会关最后一次请求的 body,前 N−1 次全泄漏。
错误写法:
for i := 0; i <p>正确解法有两种:</p>
- 拆成独立函数,让
defer在每次调用后生效 - 显式调用
resp.Body.Close(),确保每次迭代都立即释放
推荐后者(更直白、无作用域陷阱):
for i := 0; i <p>如果需要读 body,可在读完后 defer,但必须确保 defer 所在作用域与 resp 生命周期一致(比如放在 if 块内)。</p><h3>403/500 等非 2xx 响应体也必须关闭</h3><p>很多人只在 <code>resp.StatusCode == 200</code> 时读 body 并关,但 HTTP 协议规定:只要返回了响应,服务端就可能写了 body(比如 403 返回错误详情、500 返回堆栈)。不读不关,连接照样卡住。</p><p>正确逻辑是:只要 <code>err == nil</code>,就说明 TCP 连接已建立、HTTP 响应头已收到,此时 <code>resp.Body</code> 一定非 nil,必须关闭。</p><p>不要这样写:</p><pre class="brush:php;toolbar:false;">if resp.StatusCode == 200 {
defer resp.Body.Close()
// ...
}
要这样写:
if err != nil {
return err
}
defer resp.Body.Close() // ✅ 无论 status 是多少
if resp.StatusCode != 200 {
// 可选:io.Copy(io.Discard, resp.Body) 消费 body 防止阻塞
return fmt.Errorf("HTTP %d", resp.StatusCode)
}
// 正常处理 body
真正容易被忽略的点是:非 2xx 响应也可能携带大量 body 数据,不消费+不关,一样触发连接泄漏。别凭状态码做“是否需要关”的判断。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











