Go 的 http.ResponseWriter 不持用户缓冲区,底层有约4KB缓冲;Write() 不立即发送数据,需显式 Flush() 才触发网络发送;Content-Length 存在则禁用 chunked 编码,否则自动启用。

Go 的 http.ResponseWriter 本身不持有用户数据缓冲区,但底层 HTTP server 实现自带小缓冲(约 4KB),且是否启用 chunked 编码完全由 Content-Length 是否存在决定——不是你“开启”或“关闭”的功能,而是它自动推导的行为。
为什么 Write() 后客户端收不到数据?
因为 Write() 只是把字节写入 server 内置的输出缓冲区,并不触发网络发送。尤其在流式场景(如 SSE、日志推送、长响应)中,数据卡在缓冲里,客户端永远等不到第一个 chunk。
- 必须显式检查并调用
Flusher:if f, ok := w.(http.Flusher); ok { f.Flush() } - 循环中每次
Write()后都应Flush(),否则只有缓冲填满或 handler 返回时才发出去 -
Flush()可能返回broken pipe(客户端已断开),建议简单忽略或记录,不要 panic
Content-Length 存在时 chunked 就失效
只要你在 WriteHeader() 之后、Write() 之前设置了有效的 Content-Length(比如 w.Header().Set("Content-Length", "1024")),Go 就会删掉 Transfer-Encoding: chunked,改用固定长度传输。
- 中间件(如
gzip.Handler)可能悄悄设置Content-Length,导致你预期的流式行为中断 -
http.ServeFile和http.FileServer默认走Content-Length路径,不 chunked - 用
io.Copy(w, reader)时,若reader实现了Size()(如os.File),io.Copy可能触发Content-Length自动推导
io.Copy 是流式转发的安全基线
io.Copy 底层使用 32KB 固定 buffer 分块读写,内存占用恒定,天然适配 chunked 场景,且不依赖 Content-Length。
- 上游响应没设
Content-Length→ Go 自动用 chunked;设了 → Go 用 identity + length - 别用
ioutil.ReadAll或bytes.Buffer先攒全量再写,那会阻塞整个响应、吃光内存 - 注意透传 header:上游的
Content-Length若保留,会覆盖你的流式意图;建议只透传Content-Type、Content-Disposition等安全字段
真正容易被忽略的点是:chunked 不是“你要不要”,而是“你有没有拦住 Go 去设 Content-Length”。哪怕一个中间件、一次 http.Error、甚至 http.Redirect 都可能偷偷写入 Content-Length,让整个流式链路静默退化。查问题先看响应头里有没有 Content-Length,比调试 Flush() 次数更有效。











