
为什么 http.ResponseWriter 直接写入不触发 chunked?
Go 的 http.ResponseWriter 默认不会自动启用 chunked 编码——它只在你没设置 Content-Length 且响应未被缓冲(比如没用 httputil.NewSingleHostReverseProxy 这类中间件)时,由底层 net/http 在写入完成后判断是否需要 chunked。但“写入完成”不等于“每次 Write”,而是指 handler 返回、或显式调用 Flush() 且底层支持流式。
常见错误现象:curl -N http://localhost:8080/stream 一直卡住,直到 handler 结束才一次性吐出全部数据;浏览器 Network 面板里 Response Headers 没有 Transfer-Encoding: chunked。
- 必须显式调用
flusher, ok := w.(http.Flusher)判断是否支持 flush(如标准 HTTP/1.1 server 支持,但某些 reverse proxy 或测试用的httptest.ResponseRecorder不支持) - 不能只写
w.Write([]byte{...})就完事,得紧跟着flusher.Flush() - 如果提前写了
Content-Length,chunked 会被禁用,哪怕你后续 flush —— Go 会直接忽略 flush 请求
怎么安全地做 JSON 行流(NDJSON)分发?
很多后端要推实时日志、事件流或 SSE 前身,习惯用换行分隔 JSON 对象(即 NDJSON)。这时候不是简单拼字符串,得控制好 MIME 类型、编码头和刷新节奏。
使用场景:前端用 fetch().body.getReader() 流式解析;或 curl 查看实时输出;避免内存堆积(尤其日志量大时)。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 响应头必须设为
w.Header().Set("Content-Type", "application/x-ndjson; charset=utf-8")(注意不是application/json,后者暗示单个完整对象) - 不要设
Content-Length,否则 chunked 失效 - 每写一行 JSON 后立即
flusher.Flush();若用json.Encoder,记得对它的Encoder.Encode()后 flush,而不是只 flush 底层 writer - 注意
json.Encoder默认会在每个对象后加换行,符合 NDJSON 规范,不用手动w.Write([]byte{'\n'})
http.CloseNotify 已弃用,那怎么检测客户端断连?
Go 1.8+ 中 http.CloseNotify() 被标记为 deprecated,因为其行为在 HTTP/2 和某些代理下不可靠。但流式传输最怕客户端中途关闭连接却服务端还在写——会触发 write: broken pipe 错误,甚至 panic(如果没 recover)。
正确做法是监听 Request.Context().Done(),它在客户端断开、超时、取消时都会被 close。
- 每次
Write或Flush前,先检查if err := r.Context().Err(); err != nil { return } - 不要依赖
flusher.Flush()的 error 判断断连——它可能返回nil即使连接已死(尤其在 TCP FIN 后的一小段时间内) - 如果 handler 里有长循环(如轮询 DB),建议用
select { case 主动退出
用 io.Pipe 做异步流是个坑
有人想把生成数据的逻辑和 HTTP 写出解耦,就用 io.Pipe() 启个 goroutine 往 Writer 写,主 goroutine 从 Reader 读并 flush。这看似优雅,实则极易出问题。
性能与兼容性影响:pipe buffer 默认 64KB,一旦下游消费慢(如网络卡顿),writer goroutine 会阻塞在 Write(),进而卡住整个数据生产流程;更糟的是,如果 client 断连,io.Pipe 的 Read() 可能永远不返回 error,导致 goroutine 泄漏。
- 除非你能严格控制数据生产速率 & 有超时机制,否则别用
io.Pipe包一层 - 更稳妥的做法是:在 handler goroutine 内同步生成 + 写 + flush,用
context.WithTimeout控制整体生命周期 - 如果真要异步(比如拉第三方 API 流),请用带 cancel 的 channel +
select,而不是 pipe
真正难的从来不是“怎么让数据一节节发出去”,而是“怎么在发一半时干净利落地停住,还不惊动其他请求”。Context 取消、flush 判定时机、encoder 与 writer 的边界——这些地方稍微松一点,线上就容易出现半截响应、goroutine 堆积、连接泄漏。

















