登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

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 并不是自动提供的重放入口,不能照搬客户端假设。

构造类型与正文重建方法的静态关系
图1:支持的具体 Reader 可让构造器设置重建方法;包装流需要单独约定。图为静态结构,不是网络流程或截图。

小型正文优先在发送前保留不变副本

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 混为一谈。

可重放正文与业务重试许可的静态边界
图2:正文重建、尝试资源和业务许可分别负责不同前提;没有表示“GetBody非空就能重试”的关系。

GetBody 非空以后,仍不要直接循环重试 POST

307、308 等需要保留方法和正文的重定向可能用到 GetBody,这与应用自己判断是否重试不是同一层。标准 Transport 也只在文档规定的网络错误、连接状态和幂等前提下进行有限范围的重试,不是对所有失败 POST 的自动重试器。

Transport 文档把特定方法或具有幂等键头的请求视为符合其幂等判定;但加一个 Idempotency-Key 头不会自动让服务端实现去重。必须有真实的服务端契约,包括相同操作如何识别、重复请求如何返回结果,才能据此设计客户端重试。本文不提供“报错就重发”的发送循环。

修复后按三层口径理解结果

检查对象应能说明什么不能推出什么
构造来源正文副本或可稳定重建的来源已保留不证明操作幂等
GetBody 工厂每次读取位置独立,字节与长度一致不证明服务端没处理首请求
业务契约哪些失败允许再次尝试不替代资源释放与时间预算

读者可以在自己的受控测试中分别创建两份 Body,确认它们独立读出完整预期内容,并覆盖工厂返回错误、上下文已取消和空正文这几种情况。本文没有执行这些测试,不提供虚构的成功日志。

只设置 ContentLength 能补出 GetBody 吗? 不能,长度不包含重建方法。

直接 Seek 回原来的 reader 可以吗? 对未共享、未关闭的特定来源可能可行,但它不是通用重试工厂;独立句柄更容易说明并发与关闭边界。

GetBody 为空就完全不能重试吗? 如果调用方在请求之外保存了可靠的构造数据,可以重新创建一份完整请求;若没有稳定来源,就应承认该正文不可重放,而不是发送空内容凑一次尝试。

修复顺序是先保留稳定数据,再构造独立正文,最后依据业务契约决定是否发送。GetBody 是其中的数据能力,不是整套重试策略。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>