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

Go net/http Flusher 如何安全发送增量响应

来源:17golang原创

时间:2026-09-15 13:03:46 165浏览 收藏

做 SSE、日志推送或长轮询时,很多人把 Flush() 当成“客户端马上能看到”。更稳妥的做法是:先在第一次写入前完成响应头,再运行时判断 ResponseWriter 是否支持刷新;发送循环同时监听请求取消和写入错误。这样能保证服务端确实把当前缓冲交给连接,但不能越过反向代理的额外缓冲。

官方文档:https://pkg.go.dev/net/http

要点速览
  • Flusher 是可选能力,包装器可能把它隐藏,不能直接强制断言。
  • 响应头和状态要在首个 WriteFlush 前准备好,每个事件写完再刷新。
  • 客户端断开、写入失败和代理缓冲都要单独处理,Flush 成功不等于浏览器已渲染。

一、先把增量响应的边界定清楚

下面的示例采用 SSE 格式,每条消息以空行结束。Content-Type、缓存策略和状态码必须在第一次写入前设置;一旦响应提交,再改 Header 就不会产生预期效果。示例只展示服务端发送逻辑,不代表已经在某个本机环境运行。

ResponseWriter、响应头、事件数据和客户端连接的静态关系框图
图1:操作示意图,查看响应头、ResponseWriter、事件数据与客户端连接之间的静态边界。
func stream(w http.ResponseWriter, r *http.Request) {
	// SSE 需要明确内容类型,并避免中间层把响应当作可缓存内容。
	w.Header().Set("Content-Type", "text/event-stream")
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("X-Content-Type-Options", "nosniff")

	// 通过控制器获得带错误返回的 Flush,适合 Go 1.20 及以上。
	controller := http.NewResponseController(w)
	if err := controller.Flush(); err != nil {
		// 不支持刷新时不要假装是流式响应,直接返回错误。
		http.Error(w, "streaming is not supported", http.StatusInternalServerError)
		return
	}
}

ResponseController.Flush 会尝试调用底层的 FlushFlushError,不支持时返回错误。若项目需要兼容更早版本,也可以用 flusher, ok := w.(http.Flusher) 做能力判断;不要把所有包装器都当作原生响应写入器。

二、每条消息写完再刷新,并处理写入失败

刷新应跟在一条完整消息之后,而不是把半截 JSON 或半行 SSE 内容交给客户端。下面把“写入”和“刷新”放在同一轮中,并检查写入错误;Flush 本身不负责重试,也不替你恢复断开的连接。

func stream(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/event-stream")
	w.Header().Set("Cache-Control", "no-cache")

	controller := http.NewResponseController(w)
	if err := controller.Flush(); err != nil {
		http.Error(w, "streaming is not supported", http.StatusInternalServerError)
		return
	}

	ticker := time.NewTicker(time.Second)
	defer ticker.Stop() // 退出 Handler 时释放定时器资源。

	for seq := 1; seq 

首个空刷新用于尽早提交响应头,循环中的刷新用于提交当前事件。生产代码还应限制单次消息大小,避免把大对象一次写入;如果下游任务本身支持 context.Context,要把 r.Context() 继续传入,而不是重新创建一个无法取消的 context.Background()

三、为什么“服务端 Flush 了”客户端仍然收不到

官方文档明确提醒:即使响应写入器支持 Flush,HTTP 代理仍可能缓存数据,直到响应结束才转发。排查时把链路拆成三段:Handler 是否写入完整事件,Go 服务是否执行刷新,代理和客户端是否继续缓存。只看到服务端日志里的“已发送”不能证明浏览器已经收到。

请求上下文取消、Flush、代理缓冲与客户端可见性的静态关系框图
图2:结果示意图,查看请求上下文、Flush、代理缓冲和客户端可见性之间的静态关系。

测试时可先直连 Go 服务,再逐层加上反向代理;客户端用能逐块读取响应的方式观察空行分隔的消息。如果直连能够增量到达、经过代理后变成一次性到达,问题就在链路策略而非 Flusher 判断。还要注意 http.TimeoutHandler 不支持 Flusher,它不适合作为这种流式 Handler 的外层包装。

四、上线前的最小检查清单

  • 首个写入前设置内容类型、缓存和必要的安全响应头。
  • 对原始或包装后的 ResponseWriter 做运行时能力判断,优先保留 ResponseController.Flush 的错误。
  • 每个完整事件写完后刷新,并在下一轮前监听 r.Context().Done()
  • 分别验证直连、代理、HTTP/1.x 和 HTTP/2;不要把代理是否立即转发归因给 Go 的 Flush。
  • Handler 返回后不再使用 ResponseWriter,也不要从另一个 goroutine 继续写同一个响应。

相关问题

http.Flusher 和 ResponseController.Flush 该选哪个?

新代码可优先使用 ResponseController.Flush,因为它能返回错误并适配底层的 FlushError;需要兼容旧 Go 版本时,再用 http.Flusher 做显式能力判断。

Flush 会自动让浏览器立刻显示内容吗?

不会。它只负责把 Go 服务端当前缓冲交给连接,代理、客户端读取方式和浏览器渲染策略仍可能造成延迟。

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