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

Go http.ResponseController.EnableFullDuplex 什么时候该用:流式请求体与响应读取边界

来源:17golang原创

时间:2026-08-28 04:32:52 296浏览 收藏

做一个“上传一段内容,服务端边读边回传处理进度”的 Handler 时,最容易踩到的坑不是 goroutine,而是 HTTP 服务端默认会怎样处理请求体。结论是:如果 Handler 需要在还没读完 Request.Body 时就写 ResponseWriter,可以先用 http.NewResponseController(w).EnableFullDuplex() 明确开启全双工;调用必须发生在第一次写响应之前,而且要检查返回的 error。

EnableFullDuplex 解决的是“请求体读取和响应写出能否交错进行”,不是压缩、缓存或网络加速按钮。HTTP/2 默认支持这种读写并行;HTTP/1 下如果不主动启用,服务端可能先缓冲请求体,导致客户端迟迟看不到中途响应。

要点速览
  • 只在服务端 Handler 需要边读 Request.Body 边写 ResponseWriter 时考虑它。
  • 先调用 EnableFullDuplex,再设置响应头、写状态码或写正文。
  • 返回 error 时不要假设底层协议支持全双工,应按业务决定降级或直接返回错误。
  • HTTP/2 本身支持全双工,但仍要控制读取上限、客户端断开和响应节奏。

问题现场:为什么中途写出的响应没有马上到客户端

假设请求体是一条较大的 JSON 流,Handler 每读到一段就写一个进度标记。代码看起来很直白:

buf := make([]byte, 4*1024)
for {
    n, err := r.Body.Read(buf)
    if n > 0 {
        fmt.Fprintf(w, "read=%d\n", n)
        if f, ok := w.(http.Flusher); ok {
            f.Flush()
        }
    }
    if err == io.EOF {
        break
    }
    if err != nil {
        return
    }
}

问题在于,HTTP/1 服务端通常要避免在请求体还没读完时就把响应发出去。于是 FprintfFlush 并不一定意味着客户端立刻收到这一段。这个现象不能靠增加 flush 次数解决,先要处理读写时序。

Go ResponseController 通过 EnableFullDuplex 连接 Request Body 读取与 ResponseWriter 写出调用链

先把 EnableFullDuplex 放到第一次写响应之前

最小改动是创建 ResponseController 并立即启用全双工。这里的“立即”很重要:一旦 Handler 已经写过响应头或正文,再改变读写模式就晚了。

func stream(w http.ResponseWriter, r *http.Request) {
    controller := http.NewResponseController(w)
    if err := controller.EnableFullDuplex(); err != nil {
        http.Error(w, "full duplex is not supported", http.StatusHTTPVersionNotSupported)
        return
    }

    w.Header().Set("Content-Type", "text/plain; charset=utf-8")
    flusher, ok := w.(http.Flusher)
    if !ok {
        http.Error(w, "streaming is not supported", http.StatusInternalServerError)
        return
    }

    buf := make([]byte, 4*1024)
    for {
        n, readErr := r.Body.Read(buf)
        if n > 0 {
            fmt.Fprintf(w, "read=%d\n", n)
            flusher.Flush()
        }
        if readErr == io.EOF {
            return
        }
        if readErr != nil {
            return
        }
    }
}

这段代码的关键链路是 ResponseControllerEnableFullDuplexRequest.BodyResponseWriter 交错读写。全双工成功后,仍然需要单独判断 http.Flusher;前者是读写时序能力,后者是把已写内容尽快推向客户端的接口能力,不能混为一谈。

默认缓冲和全双工的分界在哪里

可以把一次请求想成两条同时前进的路径:一条从 Request.Body 读取,另一条向 ResponseWriter 写进度。在 HTTP/1 下,默认的 buffering 行为可能先把请求体读完,再处理响应;启用 full duplex 后,服务端允许两条路径交错。HTTP/2 默认支持全双工,所以这个调用更多是把 Handler 的意图写清楚,并让不支持的底层实现显式返回错误。

这不等于“任何情况下都应该调用”。如果 Handler 必须先完整校验请求体,再一次性返回结果,启用全双工只会增加理解成本;如果响应需要经过代理缓冲,客户端也未必能看到每个小片段。

HTTP/1 默认 buffering 与启用 full duplex、HTTP/2 全双工状态分支对照图

错误分支要留在协议边界内处理

EnableFullDuplex 会把底层 ResponseWriter 不支持该能力的情况交给调用方。不要把这个错误吞掉后继续写,因为此时你无法再把“客户端会收到中途进度”当成契约。

  • 启用失败:返回明确的 5xx 或按接口约定降级为完整读取后一次性响应。
  • 读取失败:客户端断开、请求体损坏或超出上限时停止继续写,避免把错误进度当作成功。
  • 写入失败:客户端已经断开时,后续 Flush 或写操作的错误应进入日志和指标。
  • 资源边界:对不可信请求使用 http.MaxBytesReader 或应用层计数,不能因为能流式读取就取消大小限制。

还有一个次序细节:响应头一旦写出,状态码和部分 Header 就基本定型。因此要先完成能力探测和必要的请求约束,再写 Header 和正文。

用一个可观察的测试检查读写是否交错

测试不要只断言最终响应字符串。更有价值的是让请求体分两次提供数据,Handler 在每次读到数据后写一条记录,再检查服务端确实走过了两次读取和两次写出。使用 httptest 可以验证 Handler 的逻辑,但是否经过真实 HTTP/1 网络缓冲,还需要单独的端到端测试环境。

func TestStream(t *testing.T) {
    req := httptest.NewRequest(http.MethodPost, "/stream", strings.NewReader("part-one-part-two"))
    rec := httptest.NewRecorder()

    stream(rec, req)

    if rec.Code != http.StatusOK {
        t.Fatalf("status = %d", rec.Code)
    }
    if !strings.Contains(rec.Body.String(), "read=") {
        t.Fatalf("body = %q", rec.Body.String())
    }
}

验收至少覆盖三件事:调用发生在第一次写响应前;启用失败时不会继续输出伪进度;读到 io.EOF 后 Handler 能正常结束。若要验证“客户端能否在上传未完成时看到进度”,应启动真实 httptest.Server,使用一个可控的分段 io.Reader,再分别走 HTTP/1 和 HTTP/2 配置。

相关问题

EnableFullDuplex 会自动把响应变成流式响应吗?

不会。它处理的是服务端读请求体和写响应的并行边界;是否及时发送还涉及 http.Flusher、协议实现和中间代理。

已经调用了 Flush,还需要 EnableFullDuplex 吗?

如果请求体还在读取,且目标是 HTTP/1 下交错读写,只调用 Flush 不足以表达这个意图。应先启用全双工,再按需要 Flush。

启用失败后可以忽略吗?

只有当接口允许退化为完整读取、一次性响应时才可以降级;如果客户端依赖中途进度,就应把失败作为能力不满足处理。

流式读取是不是就不需要限制请求体大小?

不是。流式只改变读取时机,不改变输入可能无限增长的风险;仍应设置请求大小上限,并在读取、写出和客户端断开时做清理。

最后的判断

当 Handler 真正需要“请求体尚未读完,响应已经要开始写”时,ResponseController.EnableFullDuplex 才有明确价值。把它放在第一次写响应前,检查返回值,再把 Flush、大小限制和断开处理分别做好,才能把一次偶尔出现的缓冲现象变成可解释、可测试的服务行为。

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