resp.Body.Close() 不能直接 defer 是因为 resp 可能为 nil,导致 panic;必须先检查 err != nil 再 defer;且需完整读取或丢弃 body 才能复用连接,同时必须校验 StatusCode。

resp.Body.Close() 为什么不能直接 defer 就完事
因为 resp 可能是 nil。当 http.Client.Do() 遇到 DNS 失败、连接超时或 TLS 握手失败等底层错误时,err != nil 且 resp == nil。此时写 defer resp.Body.Close() 会触发 panic:invalid memory address or nil pointer dereference。
正确做法是在确认 resp 非 nil 后再 defer:
- 先检查
if err != nil { return err } - 再写
defer resp.Body.Close()(此时resp必然非 nil) - 若需在 error 分支也做 cleanup(如日志后清理),应显式判空:
if resp != nil && resp.Body != nil { resp.Body.Close() }
只 Close() 不读完 body 会导致连接无法复用
Go 的 http.Transport 要求:必须完整消费 resp.Body(读到 EOF)或主动丢弃,才能把 TCP 连接放回空闲池。否则连接会被标记为“污染”,下次复用时可能读到上一次的残留数据,或直接被关闭——最终表现为端口耗尽、dial tcp: can't assign requested address。
常见错误场景:
- 用
json.NewDecoder(resp.Body).Decode(&v)解析后直接Close():解码器只读所需字节,body 剩余部分未处理 - 调用
io.ReadAll(resp.Body)但发生 error 后没关(或没读完就 panic) - 完全不需要响应体,却没做任何读取操作
安全做法:
- 需要内容:用
io.ReadAll(resp.Body)或流式处理 + 显式丢弃剩余(如decoder.More()后io.Copy(io.Discard, resp.Body)) - 不需要内容:必须
io.Copy(io.Discard, resp.Body),再Close()
高并发下默认 Transport 参数会加剧连接泄漏
即使你每处都正确关闭了 resp.Body,如果 http.Client 复用不当,照样会端口耗尽。关键陷阱在 http.Transport 的默认值:
-
MaxIdleConnsPerHost默认仅2:哪怕只访问一个域名,最多只保留 2 个空闲连接;其余连接响应结束后立即断开 -
MaxIdleConns默认100:全局上限低,突发流量容易打满 - 不设
IdleConnTimeout:空闲连接永远不回收,可能堆积无效连接
生产环境建议显式配置:
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 200,
MaxIdleConnsPerHost: 50,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
},
}
StatusCode 检查漏掉会导致“假成功”并掩盖连接问题
很多人只写 if err != nil 就认为请求没问题,结果 404/500 响应体被照常读取、关闭——看似流程走通,实则业务逻辑已错,且因错误响应体往往更短、更易被忽略,反而让连接复用问题更隐蔽。
必须显式判断状态码:
- 用
if resp.StatusCode = 300或resp.StatusCode >= 400 - 非 2xx 响应体通常含错误详情(如 JSON 错误信息),建议读出并包装进自定义 error
- 304 响应体为空,直接
io.ReadAll(resp.Body)会返回空 slice +nilerror,但若没检查状态码,后续解析可能 panic
真正安全的响应处理,三件事缺一不可:resp.Body.Close()、resp.StatusCode 检查、resp.Body 完整消费。少一步,连接池就多一分风险。











