因为Write()仅写入约4KB内存缓冲区而不触发网络发送,需显式断言http.Flusher并调用Flush()才能实时推送;若未设Content-Length则自动启用chunked编码,但Nginx等代理默认缓冲需禁用proxy_buffering。

为什么 w.Write() 后客户端收不到数据
Go 的 http.ResponseWriter 默认启用 HTTP/1.1 响应缓冲,Write() 只是把字节写入内存 buffer(通常约 4KB),不会立即发到 TCP 层。流式场景下,用户卡在 loading,实际数据早就 Write() 过了——问题就出在这层缓冲。
必须显式触发刷新,且不能假设 fmt.Fprintf(w, ...) 或 json.NewEncoder(w).Encode() 自动 flush。
- 缓冲区未满时,
Write()不发送; - handler 返回时才强制 flush 整个 buffer,这违背“逐行推送”意图;
- 若用了
gzip.Handler等中间件,它可能内部缓冲整个 body,直接禁用 flush 能力(静默失败)。
怎么安全调用 Flush():类型断言是必选项
http.Flusher 不是 ResponseWriter 的强制接口,很多环境根本不支持:Nginx 默认反代、Vercel、Cloudflare Workers、httptest.ResponseRecorder 都不实现它。直接 w.(http.Flusher).Flush() 会 panic。
正确做法永远先做类型断言:
立即学习“go语言免费学习笔记(深入)”;
if f, ok := w.(http.Flusher); ok {
f.Flush()
} else {
// 记录日志或降级处理,比如返回错误
http.Error(w, "streaming not supported", http.StatusInternalServerError)
return
}
- 别在
w.WriteHeader()前调Flush(),否则会隐式写入 200 状态,后续再设 header 就 panic; - 断言必须在每次需要 flush 前重复做——中间件可能包装了
w,导致类型变化; - 某些测试框架(如
gin-gonic/gin/test)返回的 writer 也不支持,本地测通 ≠ 线上可用。
SSE 格式与响应头设置缺一不可
即使 flush 成功,客户端也可能收不到事件,常见原因是响应头或格式不合规。SSE 要求严格:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
必须设置:
-
w.Header().Set("Content-Type", "text/event-stream")(不是application/json); -
w.Header().Set("Cache-Control", "no-cache")(防止浏览器缓存整条流); -
w.Header().Set("Connection", "keep-alive")(维持长连接); - 首次响应建议写一个空行:
fmt.Fprint(w, "\n"),避免旧版 Safari 卡住解析。
每条事件必须以 data: 开头,结尾用两个 \n:
fmt.Fprintf(w, "data: %s\n\n", msg) f.Flush()
- 不要漏掉末尾的双换行,否则浏览器不触发
onmessage; - 如果传结构体,推荐
json.NewEncoder(w).Encode(v),比手动拼 JSON 更安全; - 避免在 event data 中混用
id:、event:等字段,除非你真需要事件重连或分类——简单推送只用data:就够。
客户端断连和超时怎么不炸掉服务
流式 handler 绑定的是请求的 context.Context,一旦 client 关闭连接或超时,ctx.Done() 触发,后续 Write() 或 Flush() 会返回 context canceled 或 broken pipe 错误。
不处理这些错误会导致 goroutine 泄漏或 panic:
- 每次
Flush()后检查 error:if err := f.Flush(); err != nil { return }; - 循环中加
select监听ctx.Done(),及时退出; - 避免用
time.Sleep()控制节奏——它不响应 cancel,要用time.AfterFunc或timer := time.NewTimer()+select; - 别用
http.TimeoutHandler包裹流式 handler,它会在超时后硬 kill 连接,导致流中断。
最易被忽略的一点:Nginx 默认 proxy_buffering on,它会等完整响应才透传,必须显式关掉:proxy_buffering off;,否则所有 Go 侧的努力都白费。

















