必须关闭 resp.body 以防止 goroutine 和文件描述符泄漏;只要 resp 非 nil 且 resp.body 非 nil,就必须在读完或明确放弃后调用 close(),推荐使用判空 defer 或显式关闭。

不关闭 Response.Body 会泄漏 goroutine 和文件描述符
直接丢弃 resp.Body 而不调用 Close(),会导致底层 TCP 连接无法释放,复用失败,后续请求可能卡住或报错 http: aborting pending body read。Go 的 http.Transport 默认启用连接复用(keep-alive),但前提是上一个响应的 body 被读完或显式关闭。
- 即使你只读前几个字节(比如用
io.ReadFull检查 header),也必须resp.Body.Close() - 如果
resp.Body == nil(如某些错误路径下),调用Close()会 panic,需判空 -
defer resp.Body.Close()看似安全,但在 error 分支提前 return 时容易漏掉——尤其当resp是 nil 时 defer 不执行
正确关闭的三种典型写法及适用场景
核心原则:只要 resp 非 nil 且 resp.Body 非 nil,就必须关;顺序上,先读完 body(或明确放弃),再 Close()。
- 通用安全写法(推荐):
resp, err := http.Get("https://example.com") if err != nil { return err } defer func() { if resp.Body != nil { resp.Body.Close() } }() // … 使用 resp.Body - 配合
io.Copy或完整读取时:body, _ := io.ReadAll(resp.Body) resp.Body.Close() // 必须在 ReadAll 后立即关,不能 defer(否则可能被延迟到函数末尾,而 body 已被消费)
- 流式处理(如下载大文件):
defer resp.Body.Close() // 此处 safe,因为 resp 非 nil 且 Body 可被 defer 正确清理
但注意:若中间有 return,且 resp.Body 未被读完,连接仍可能滞留——此时应改用io.Copy+ 显式Close
resp.Body.Close() 调用后还能读吗?
不能。调用 Close() 后再次读 resp.Body 会返回 nil, io.ErrClosedPipe 或类似错误。但更关键的是:关闭本身不等待读操作完成——如果你在 Close() 前没读完,剩余数据会被丢弃,底层连接可能被强制断开。
- 不要依赖
Close()来“中断”读取;要用context.WithTimeout控制整个请求生命周期 -
http.DefaultClient的超时仅作用于连接和首字节到达,不约束 body 读取;body 读取超时需额外处理(如用io.LimitReader或自定义context.Context) - 某些服务(如 S3)返回的
resp.Body是带缓冲的 reader,Close()可能触发校验或清理逻辑,跳过会导致校验失败或资源泄漏
常见误用:把 resp.Body 传给其他函数却不关
比如封装一个 fetchJSON(url string, v interface{}) error 函数,内部解码后忘记关 Body,调用方根本意识不到要关——这是最隐蔽的泄漏源。
- 所有返回
*http.Response的函数,责任边界必须清晰:谁创建,谁关(除非文档明确说明由调用方负责) - 若函数内部消耗了
resp.Body(如json.NewDecoder(resp.Body).Decode(v)),应在函数末尾resp.Body.Close(),而非留给调用方 - 使用
httputil.DumpResponse(resp, true)会读完整个 body,之后必须Close();它不会自动帮你关
Close(),而是不同分支下 resp 和 resp.Body 的 nil 状态不一致,以及 defer 在 error 处理中的失效。多一层判空,少一次线上排查。











