
HTTP 响应体(resp.Body)是单次可读的 io.ReadCloser,首次读取后即关闭或耗尽,再次传入将返回空数据;需先完整读取并缓存为字节切片或字符串,再多次传递给解析函数。
http 响应体(`resp.body`)是单次可读的 `io.readcloser`,首次读取后即关闭或耗尽,再次传入将返回空数据;需先完整读取并缓存为字节切片或字符串,再多次传递给解析函数。
在 Go 中,http.Response.Body 并非普通内存变量,而是一个流式、一次性读取的 I/O 接口(具体类型为 io.ReadCloser)。这意味着:它只能被完整读取一次。一旦 parseGoQuery 或任何其他函数内部调用了 io.ReadAll(b)、json.NewDecoder(b).Decode(...)、gq.DocumentFromReader(b) 等读取操作,底层的 body 数据就会被消耗殆尽,缓冲区清空,后续对同一 b 的读取将立即返回 0, io.EOF —— 这正是你观察到第二次调用时 b “变成 0”的根本原因。
✅ 正确做法:提前读取并复制响应体内容到内存
使用 io.ReadAll 将整个响应体读入 []byte,然后基于该字节切片创建多个 bytes.Reader(实现 io.Reader),供多次解析使用:
resp, err := client.Get(URL)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close() // 注意:此处关闭 resp.Body,而非 b
// 一次性读取全部响应体
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal("failed to read response body:", err)
}
// 多次安全调用解析函数(每次传入新的 bytes.Reader)
parseGoQuery("tag1", bytes.NewReader(bodyBytes))
parseGoQuery("tag2", bytes.NewReader(bodyBytes))
⚠️ 关键注意事项:
-
不要重复
defer b.Close():resp.Body只需关闭一次,且应在读取完成后立即关闭(如上例),避免资源泄漏; -
避免直接传递
resp.Body多次:即使未显式读取,某些解析库(如goquery)会在内部调用ReadAll或Read,导致隐式耗尽; - 大响应体需谨慎:若响应体积过大(如 GB 级文件),不宜全量加载至内存,应改用流式处理或临时文件缓存;
-
bytes.NewReader()是零拷贝封装:它仅持有对[]byte的引用,不额外复制数据,性能高效。
? 总结:Go 的 I/O 设计强调明确性与可控性。resp.Body 的“单次读取”特性不是缺陷,而是防止意外重复读取、提升资源管理透明度的设计选择。始终遵循「读一次 → 存内存 → 多次复用」模式,即可稳健支撑多阶段解析、日志记录、重试等常见需求。










