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

EnableFullDuplex 为什么只对部分协议有效

来源:17golang原创

时间:2026-10-09 20:47:19 177浏览 收藏

在一个需要边读上传内容、边返回处理进度的 Handler 里,开发者常会看到 EnableFullDuplex 只在某些请求上产生明显效果。原因不是 API 随机失效,而是 Go 的 HTTP/1 与 HTTP/2 默认行为不同:HTTP/1 需要它来阻止服务端先吞掉未读请求体,HTTP/2 本来就允许请求读取和响应发送并行。

要点速览
  • EnableFullDuplex 的主要收益在 HTTP/1 请求,HTTP/2 调用它通常是冗余的。
  • ResponseController 会沿着 Unwrap 查找能力,包装器没有暴露该方法时可能返回 ErrNotSupported。
  • 真正的流式效果还受 Flush、代理缓冲和 Handler 生命周期影响,不能只看调用是否返回 nil。

先按协议解释“只对部分请求有效”

Go 官方文档把这个方法定义为:Handler 会交错读取 Request.Body 并写入 ResponseWriter。对 HTTP/1 请求,服务端默认会在开始写响应前消费剩余请求体;调用它会关闭这项默认行为,让 Handler 继续读请求体并同时写响应。HTTP/2 则始终允许读取请求体与发送响应,所以同一个调用不会像 HTTP/1 那样改变明显的时序。

HTTP/1 与 HTTP/2 对 EnableFullDuplex 作用边界的静态说明图
图1:协议边界说明图,展示 HTTP/1 的默认消费行为与 HTTP/2 的原生并行能力;这是原创静态说明图,不是运行截图。

因此排查时先记录 r.Proto,不要看到 HTTP/2 没有变化就判断 API 失效。若前面还有反向代理,也要把“服务端已经写出”和“客户端已经看到”分开,代理可能继续缓冲响应。

在原始 ResponseWriter 上调用并处理错误

NewResponseController 最好接收 Handler 收到的原始 ResponseWriter。如果中间件包装了它,Controller 只会调用包装器提供的方法,或沿着它的 Unwrap 返回原始对象;找不到能力时返回匹配 http.ErrNotSupported 的错误。

func enableDuplex(w http.ResponseWriter) error {
	// Controller 会尝试调用原始 writer 或包装器 Unwrap 后的能力。
	controller := http.NewResponseController(w)
	if err := controller.EnableFullDuplex(); err != nil {
		// 不支持时不要伪装成已开启;调用方可以按协议选择降级。
		if errors.Is(err, http.ErrNotSupported) {
			return fmt.Errorf("当前 ResponseWriter 不支持全双工: %w", err)
		}
		return fmt.Errorf("启用全双工失败: %w", err)
	}
	return nil
}

这里的错误不是“HTTP/2 不支持”的证据。HTTP/2 的原生 writer 已经允许读写并行;真正出现 ErrNotSupported 时,优先检查自定义 ResponseWriter 是否遗漏了 EnableFullDuplex() error 或 Unwrap() http.ResponseWriter。

把读取、写入和 Flush 放进同一条链路

启用能力后,还需要设计实际的读写循环。下面的示例把请求体分块读入 channel,主协程写出进度,并在每块之后尝试刷新;它表达的是交错关系,不代表任何代理都会立刻把字节交给浏览器。

func streamUpload(w http.ResponseWriter, r *http.Request) {
	// 只有 HTTP/1 的默认请求体消费时机需要这一步改变。
	controller := http.NewResponseController(w)
	if r.ProtoMajor == 1 {
		if err := controller.EnableFullDuplex(); err != nil {
			// HTTP/1 需要显式启用;不支持时应在写出响应前降级。
			http.Error(w, "full duplex unavailable", http.StatusNotImplemented)
			return
		}
	}

	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
	chunks := make(chan []byte)
	readErr := make(chan error, 1)
	go func() {
		defer close(chunks)
		defer r.Body.Close() // 读取结束或客户端断开时都释放请求体。
		buf := make([]byte, 32*1024)
		for {
			n, err := r.Body.Read(buf)
			if n > 0 {
				part := append([]byte(nil), buf[:n]...) // 复制,避免下一次 Read 覆盖数据。
				chunks 
Go Handler 中请求体读取与响应写入交错的静态结构图
图2:读写交错结构图,展示 Request.Body、分块 channel、ResponseWriter 与 Flush 的关系;这是原创静态说明图,不是运行截图。

示例里的 Flush 只负责把服务端缓冲尽量向下游推进,不能绕过代理缓存;生产代码还应设置请求大小、超时和取消处理,并避免在客户端断开后继续向 channel 阻塞发送。

发布前的协议与中间件检查清单

  • 先记录 r.Proto:HTTP/1 的行为变化才是 EnableFullDuplex 的主要价值,HTTP/2 不要用“调用后输出没变化”作为故障证据。
  • 检查 Controller 的输入是否是原始 writer,或包装器是否正确实现了 Unwrap 与全双工方法。
  • 把 Flush、代理缓冲、客户端读取速度和 Handler 退出时间分开验证;返回 nil 只说明能力调用成功。
  • 若采用降级策略,必须在响应尚未写出前处理不支持错误,不能先发送 200 再改成错误状态。

常见追问

HTTP/2 还需要调用 EnableFullDuplex 吗?通常不需要。HTTP/2 服务端本来就允许请求读取与响应并行,调用可以统一代码路径,但不会带来 HTTP/1 那种关键行为变化。

调用成功后客户端为什么仍然看不到每一块?先检查是否调用了 Flush,再检查反向代理和客户端是否缓冲;EnableFullDuplex 解决的是服务端读写时序,不是端到端传输缓冲策略。

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