go 的 http.request.body 默认只能读一次,因其本质是流式 io.readcloser,底层为一次性消耗的 socket 或 buffer;标准库不自动缓存,需手动用 io.nopcloser(bytes.newreader(buf)) 复位以支持多次读取。

Go 的 net/http.Request.Body 默认只能读一次,直接反复调用 req.Body.Read() 或 ioutil.ReadAll(req.Body)(或 io.ReadAll(req.Body))会导致后续读取返回空或 EOF —— 这不是 bug,是设计使然。
为什么 Body 不能重复读?
HTTP 请求体本质是一个流式 io.ReadCloser,底层常为 socket 连接或内存 buffer,读完即消耗。Go 不做自动缓存,也不在标准库中默认复制原始字节到内存供多次消费。
常见错误现象:
- 第一次
io.ReadAll(req.Body)拿到完整 JSON,第二次再读就得到[]byte{}或EOF - 中间件解析 body 后,handler 再读不到数据,导致业务逻辑 panic 或静默失败
- 用
json.NewDecoder(req.Body)解码后,无法再以字符串形式记录原始请求日志
方案一:用 req.Body = io.NopCloser(bytes.NewReader(buf)) 复位
适用于已知 body 小、可全量载入内存的场景(如普通 API 请求,通常 ReadCloser。
实操建议:
- 先用
io.ReadAll(req.Body)把原始 body 读进[]byte - 调用
req.Body.Close()(必须,否则连接可能不释放) - 用
bytes.NewReader(buf)创建新 reader,再套一层io.NopCloser(...)构造ReadCloser - 赋值回
req.Body,后续代码即可再次读取
示例:
buf, err := io.ReadAll(req.Body)
if err != nil {
http.Error(w, "read body failed", http.StatusBadRequest)
return
}
req.Body.Close() // 关键:显式关闭原 Body
req.Body = io.NopCloser(bytes.NewReader(buf))
// 现在可以多次使用 req.Body,比如:
json.NewDecoder(req.Body).Decode(&v)
req.Body.Seek(0, 0) // 注意:bytes.Reader 支持 Seek,但 io.NopCloser 包裹后不支持!需直接用 bytes.Reader
// 所以更稳妥写法是:decoder := json.NewDecoder(bytes.NewReader(buf))
方案二:用 req.GetBody(推荐用于标准 HTTP server)
Go 1.8+ 为 *http.Request 添加了 GetBody 字段(类型为 func() (io.ReadCloser, error)),专为支持多次读取设计。标准库的 http.Server 在收到请求时,若检测到 Content-Length 明确且不大,会自动设置该字段。
使用前提与要点:
- 仅当
req.GetBody != nil时可用;POST 表单、小 JSON 常有,但流式上传(如大文件 multipart)、chunked 编码请求通常没有 - 每次调用
req.GetBody()返回一个**全新、可独立读取**的ReadCloser,互不影响 - 无需手动 Close 原 Body,但每次
GetBody()返回的 body 都需自行.Close() - 比手动
io.ReadAll + NopCloser更轻量,避免重复分配大内存
示例:
if req.GetBody == nil {
http.Error(w, "request body not reusable", http.StatusUnsupportedMediaType)
return
}
body1, err := req.GetBody()
if err != nil {
http.Error(w, "get body failed", http.StatusInternalServerError)
return
}
defer body1.Close()
io.Copy(os.Stdout, body1) // 第一次读
body2, _ := req.GetBody()
defer body2.Close()
json.NewDecoder(body2).Decode(&v) // 第二次读,完全独立
方案三:中间件中预读并注入上下文(适合 Gin / Echo 等框架)
在 Web 框架中,更自然的做法是:在第一个中间件里统一读取 body,存入 context.Context,后续 handler 直接从 ctx 取,不再碰 req.Body。
关键细节:
- 务必调用
req.Body.Close(),否则连接复用(keep-alive)下可能泄漏资源 - 若需保留原始
req.Body(比如下游代理转发),仍得用GetBody或复位方案 - Gin 用户注意:
c.Request.Body是原始对象,c.GetRawData()内部已做了GetBodyfallback,但只保证一次可用 - 自定义中间件里不要直接修改
req.Body,除非你控制所有后续 handler 的读取方式
容易被忽略的一点:即使用了 GetBody,如果请求是 chunked 编码且服务端未禁用 streaming(如设置了 Server.MaxHeaderBytes 但没设 Server.ReadTimeout),GetBody 仍可能为 nil —— 此时唯一可靠方式是提前读完并复位,或拒绝该类请求。











