必须先调用 resp.body.close() 再重赋值 resp.body,否则原始 body 持有的 bufio.reader 和 socket 缓冲区无法释放,导致 readloop goroutine 挂起、内存持续增长;io.readall 后不 close 原始 body 是根本原因。

直接读取大响应体后不释放原始 resp.Body,会导致底层连接缓冲区和 goroutine 持续驻留,内存不会随 GC 回收——这不是 GC 慢,是资源根本没被释放。
为什么 ioutil.ReadAll + io.NopCloser 会卡住内存
常见错误是读完就重赋值 resp.Body,却忘了关原始 body:
原始 resp.Body 是 *http.body,内部持有 *bufio.Reader 和未释放的 socket 缓冲区;io.NopCloser(bytes.NewBuffer(...)) 只是新建一个无副作用的 ReadCloser,但旧 body 的引用仍在,GC 不会回收它关联的网络资源。
即使你 defer 关了新 body,实际关的是空实现;而原始 body 若未显式 .Close(),readLoop goroutine 就永远挂起,堆内存持续增长。
-
io.ReadAll必须配合resp.Body.Close()才算完整释放路径 - 重赋值
resp.Body前,必须先调用resp.Body.Close() - Go 1.16+ 已弃用
ioutil,统一用io.ReadAll替代
处理大响应体时避免内存暴涨的两种策略
对 >10MB 的响应,别全加载进内存。优先流式处理:
若需日志或校验后再转发,用 io.MultiReader 避免复制:
bodyBytes, _ := io.ReadAll(resp.Body)
_ = resp.Body.Close() // 关原始 body
// 构造可多次读的 body:先返回原始内容,再接空 reader 防止后续读越界
resp.Body = io.NopCloser(io.MultiReader(
bytes.NewReader(bodyBytes),
io.LimitReader( // 防止下游误读超长
io.Discard,
0,
),
))
若只需提取其中一段字符串(比如 JSON 中某个字段),别读全文:
- 用
json.Decoder流式解析,配合json.RawMessage延迟解码目标字段 - 用
bytes.Index/bytes.Split在小 buffer 中找边界,避免 hold 整个 body - 设置
http.Client.Timeout或context.WithTimeout防止大响应卡死
字符串截取引发的隐性内存泄漏
从响应体构造字符串后做 str[100:120] 这类截取,新字符串仍指向原始字节数组首地址——只要子串活着,整个原始 body 内存都无法 GC。
例如:content := string(bodyBytes) 后取 content[10:20],会导致 bodyBytes 占用的几 MB 内存一直滞留。
- 真正需要副本时,显式转换:
copyStr := string([]byte(substr)) - 或用
fmt.Sprintf("%s", substr)触发底层拷贝(更安全) - 代理/中间件中若只取 header 或 status,干脆别读 body,用
resp.Body = http.NoBody丢弃
最易被忽略的一点:所有重放响应体的逻辑,都必须把 resp.Body.Close() 放在 io.ReadAll 之后、任何重赋值之前——顺序错了,前面所有操作都白搭。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











