req.Body只能读一次是因为其底层是单次读取的io.ReadCloser流(如网络socket缓冲区),读完后偏移至末尾,再次读返回空或io.EOF;这是为避免内存缓存、支持流式处理的设计特性。

为什么 req.Body 只能读一次?
Go 的 http.Request.Body 是一个 io.ReadCloser,底层通常是网络连接的流式 reader。一旦调用 io.ReadAll(req.Body) 或 json.NewDecoder(req.Body).Decode(),数据就被消费完了——后续再读,返回空字节或 io.EOF。这不是 bug,是设计使然:避免缓冲全部请求体到内存,兼顾流式处理和资源控制。
常见错误现象:
- 第一次
io.ReadAll(req.Body)拿到 JSON 字符串,第二次再读就得到[]或 panic - 中间件里解析了一次 body,下游 handler 再解析时报
invalid character '}' after top-level value(实际是空内容被当成 malformed JSON) - 用
bytes.NewReader(buf)包装后没重置req.Body,导致 handler 仍读原始流
如何安全地重复读取 req.Body?
核心思路:把原始 body 读出来缓存成内存副本,再用 bytes.Reader 多次构造可重放的 reader。关键不是“怎么读”,而是“怎么替换并复用”。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 在中间件或入口处尽早调用
io.ReadAll(req.Body),捕获全部内容到[]byte - 用
bytes.NewReader(data)创建新 reader,并赋值给req.Body(注意要同时设置req.ContentLength,否则某些库如json.Decoder可能误判长度) - 如果原 body 很大(>几 MB),别硬缓存;改用临时文件或直接透传 + 签名/日志等轻量操作
示例片段:
body, err := io.ReadAll(req.Body)
if err != nil {
http.Error(w, "read body failed", http.StatusBadRequest)
return
}
req.Body = io.NopCloser(bytes.NewReader(body))
req.ContentLength = int64(len(body)) // 必须设,否则 json.Decode 可能出错
bytes.Reader 和 strings.Reader 有什么区别?选哪个?
两者都实现 io.Reader,但语义不同:bytes.Reader 针对 []byte,支持 Len()、Reset()、UnreadByte();strings.Reader 针对 string,底层用 unsafe.String 转换,零拷贝但不可变。对 body 缓存来说,优先用 bytes.Reader。
原因:
-
req.Body原始类型是[]byte流,用bytes.Reader类型一致,无隐式转换开销 -
bytes.Reader.Reset()可以复用 reader 实例(比如做多次 Decode),而strings.Reader不支持 Reset - 若 body 含二进制数据(如上传文件),
string(body)会破坏字节序列(UTF-8 非法序列被替换为 )
哪些情况不该缓存 body?
不是所有场景都适合用 bytes.Reader 替换 req.Body。容易忽略的边界:
- 大文件上传(如 >10MB):全量读入内存可能 OOM,应改用
io.TeeReader+ 临时磁盘缓存,或用multipart.Reader边读边处理 - 代理转发(reverse proxy):标准库
httputil.NewSingleHostReverseProxy内部已处理流式转发,手动缓存 body 会导致响应延迟和内存浪费 - 使用了
req.MultipartForm:它内部会自动调用ParseMultipartForm并缓存,此时再手动读req.Body会冲突
真正需要重复读的,通常是鉴权、审计、重试、调试等中间层逻辑,且 body 规模可控(如 JSON API 请求)。这时候才值得用 bytes.NewReader 换取语义清晰性。


















