Go 重试 POST 时 GetBody 为空怎么办
来源:17golang原创
时间:2026-10-17 18:25:49 458浏览 收藏
Go 的 POST 请求 GetBody 为 nil,通常不是“网络失败后才丢失”,而是构造时没有建立正文重建方法。小型、可保存在内存的请求体,可以直接用 bytes.NewReader 创建请求;若使用包装流,则显式提供每次返回独立 ReadCloser 的 GetBody。已经耗尽且未保存副本的一次性流,不能事后凭空恢复。
先把两个前提分开:正文可重放只解决“还能不能发送相同字节”,并不证明“服务端再次执行是安全的”。涉及创建订单、扣费等 POST,网络错误也不能证明服务端没有完成处理,业务许可必须先于重试动作。
官方文档入口:https://pkg.go.dev/net/http#Request。构造器说明:https://pkg.go.dev/net/http#NewRequestWithContext。
先检查请求最初交给构造器的类型
假设一个客户端把 JSON 先包成 io.NopCloser,再传给 NewRequestWithContext,随后发现 GetBody 为空。此时应看构造调用,而不是只盯着失败后的 Body。构造器会对 *bytes.Buffer、*bytes.Reader 和 *strings.Reader 设置 GetBody 并确定精确长度,但不会为任意 Reader 自动推断重建方式。
变量声明为 io.Reader 本身不是问题:接口内实际保存的具体类型仍可能是 bytes.Reader 指针。真正改变行为的是外层包装或另一个自定义类型让构造器不再直接看到支持的类型。对于服务端收到的请求,GetBody 并不是自动提供的重放入口,不能照搬客户端假设。

小型正文优先在发送前保留不变副本
package replay
import (
"bytes"
"context"
"fmt"
"net/http"
)
// NewTemplate 只适用于已限制大小的小型正文,私有副本保持不变。
func NewTemplate(ctx context.Context, endpoint string, payload []byte) (*http.Request, error) {
snapshot := append([]byte(nil), payload...)
req, err := http.NewRequestWithContext(
ctx, http.MethodPost, endpoint, bytes.NewReader(snapshot),
)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
return req, nil // 标准构造器为bytes.Reader设置GetBody。
}
// NewAttempt 仅准备另一次尝试,不决定业务是否允许重试。
func NewAttempt(template *http.Request, ctx context.Context) (*http.Request, error) {
if ctx == nil {
return nil, fmt.Errorf("尝试的context不能为空")
}
if err := ctx.Err(); err != nil {
return nil, err
}
if template.GetBody == nil {
return nil, fmt.Errorf("请求正文不能重建")
}
body, err := template.GetBody()
if err != nil {
return nil, fmt.Errorf("重建请求正文: %w", err)
}
if body == nil {
return nil, fmt.Errorf("GetBody返回了空正文句柄")
}
attempt := template.Clone(ctx)
attempt.Body = body // Clone不会深复制Body,必须替换。
return attempt, nil
}
这是可复用的请求构造辅助代码,不包含自动发送循环。NewTemplate 创建私有字节副本,不让调用者后续修改原 payload 影响正文。传入 endpoint 应是自己确认的接口地址;代码不会在本文制作过程中发出请求。
只有在上一次尝试已经结束、业务策略已允许继续,并且剩余时间与次数足够时,调用方才应使用 NewAttempt 准备下一份请求。模板在后续尝试期间保持不变,不在并发发送过程中改写头、正文或 GetBody。独立游标不等于可以随意并发修改模板。
确实需要包装流时,把工厂和长度一起写清楚
// 若必须使用通用ReadCloser构造请求,需要自己补齐独立正文工厂。
// 以下函数与上面的代码同包,额外导入io。
func NewWrappedTemplate(ctx context.Context, endpoint string, payload []byte) (*http.Request, error) {
snapshot := append([]byte(nil), payload...) // 不再修改捕获的字节。
reopen := func() (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader(snapshot)), nil // 每次都是新游标。
}
first, err := reopen()
if err != nil { return nil, err }
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, first)
if err != nil {
_ = first.Close() // 尚未交给Transport,构造失败自行关闭。
return nil, err
}
req.GetBody = reopen
req.ContentLength = int64(len(snapshot))
if len(snapshot) == 0 {
_ = first.Close()
req.Body = http.NoBody // 显式表达已知空正文。
req.GetBody = func() (io.ReadCloser, error) { return http.NoBody, nil }
}
return req, nil
}
第二个片段与前面的代码放在同一包,导入列表额外加入 io。它演示的是显式工厂,不需要与第一种构造方式同时使用。每次 reopen 都产生新 reader;错误写法则是反复返回同一个已经读取或关闭的 req.Body,或者让多个尝试共享一个可移动游标。
ContentLength 必须与每次重建的字节一致,不能为了让错误消失而随便填0。若原始流来自文件或动态编码,应在发送前选择可重建且内容稳定的来源,并处理重新打开失败;不要在失败后才从已消费的 Body 读取“备份”。大流也不应无上限读进内存,本文的内存副本方案只适用于已受大小限制的正文。
Clone 不是正文回卷,关闭责任也要分清
Request.Clone 对 Body 只做浅复制,所以单独克隆不能得到从头开始的正文。NewAttempt 调用 GetBody 后显式替换 Body,才使这一次尝试拥有自己的读取位置。设置 GetBody 也不能省略首次请求的 Body,两者分别表示“当前可读资源”与“以后怎样重建”。
将请求交给 Client.Do 后,请求正文由传输层负责关闭。若准备了 attempt 却决定不发送,调用方应关闭这份未交付的 Body;若模板永远只用于构造副本而不发送,也需由持有者释放模板的正文资源。本例是内存 reader,仍应保持一致的所有权习惯。收到响应时,响应正文则由消费响应的调用方关闭,不能与请求 Body 混为一谈。

GetBody 非空以后,仍不要直接循环重试 POST
307、308 等需要保留方法和正文的重定向可能用到 GetBody,这与应用自己判断是否重试不是同一层。标准 Transport 也只在文档规定的网络错误、连接状态和幂等前提下进行有限范围的重试,不是对所有失败 POST 的自动重试器。
Transport 文档把特定方法或具有幂等键头的请求视为符合其幂等判定;但加一个 Idempotency-Key 头不会自动让服务端实现去重。必须有真实的服务端契约,包括相同操作如何识别、重复请求如何返回结果,才能据此设计客户端重试。本文不提供“报错就重发”的发送循环。
修复后按三层口径理解结果
| 检查对象 | 应能说明什么 | 不能推出什么 |
|---|---|---|
| 构造来源 | 正文副本或可稳定重建的来源已保留 | 不证明操作幂等 |
| GetBody 工厂 | 每次读取位置独立,字节与长度一致 | 不证明服务端没处理首请求 |
| 业务契约 | 哪些失败允许再次尝试 | 不替代资源释放与时间预算 |
读者可以在自己的受控测试中分别创建两份 Body,确认它们独立读出完整预期内容,并覆盖工厂返回错误、上下文已取消和空正文这几种情况。本文没有执行这些测试,不提供虚构的成功日志。
只设置 ContentLength 能补出 GetBody 吗? 不能,长度不包含重建方法。
直接 Seek 回原来的 reader 可以吗? 对未共享、未关闭的特定来源可能可行,但它不是通用重试工厂;独立句柄更容易说明并发与关闭边界。
GetBody 为空就完全不能重试吗? 如果调用方在请求之外保存了可靠的构造数据,可以重新创建一份完整请求;若没有稳定来源,就应承认该正文不可重放,而不是发送空内容凑一次尝试。
修复顺序是先保留稳定数据,再构造独立正文,最后依据业务契约决定是否发送。GetBody 是其中的数据能力,不是整套重试策略。
-
502 收藏
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
122 收藏
-
366 收藏
-
199 收藏
-
283 收藏
-
275 收藏
-
159 收藏
-
242 收藏
-
116 收藏
-
453 收藏
-
130 收藏
-
427 收藏
-
256 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习