c.Stream()默认不启用chunked传输,因其ResponseWriter走标准HTTP/1.1流程,需手动WriteHeader并显式Flush才能触发分块编码;否则响应被缓冲导致卡顿。

为什么 c.Stream() 默认不启用 chunked 传输
直接调用 c.Stream() 时,Gin 不会自动设置 Transfer-Encoding: chunked,除非你手动写入响应头或触发流式判定逻辑。这是因为 Gin 的 ResponseWriter 默认走标准 HTTP/1.1 响应流程:它先缓存状态码和 Header,直到第一次 Write() 调用才真正 flush 到连接。如果流式 handler 中前几次 Write() 都被缓冲(比如因底层 TCP 窗口未打开、Nginx 代理拦截、或 Go 的 bufio.Writer 未满),浏览器就收不到任何数据,表现为“卡住”。
常见现象是:本地直连 curl -N http://localhost:8080/stream 正常输出,但加了 Nginx 后完全无响应;或者前端 EventSource 一直 pending,直到 handler 结束才一次性收到全部内容。
- 必须在首次
Write()前显式调用c.Writer.WriteHeader(http.StatusOK),否则状态行延迟发送 - 避免在流式 handler 中调用
c.JSON()、c.String()等封装方法——它们会覆盖 Header 并强制缓冲 - 确保没有中间件(如 gzip)提前包装了
ResponseWriter,否则流式写入会被截断
如何正确使用 c.Stream() 实现低延迟 SSE 推送
SSE(Server-Sent Events)本质就是带特定 Header 的长连接文本流,c.Stream() 是最轻量的实现入口。但它不是“开箱即用”,关键在于控制写入节奏和 flush 行为。
以下是最小可行模式:
func sseHandler(c *gin.Context) {
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Writer.WriteHeader(http.StatusOK) // 必须提前写出状态行
// 使用 c.Stream 避免手动管理 writer
c.Stream(func(w io.Writer) bool {
// 每次推送前,手动 flush 确保浏览器立即收到
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
_, err := fmt.Fprintf(w, "data: %s\n\n", time.Now().Format(time.RFC3339))
return err == nil
})
}
-
c.Stream()的函数参数返回true表示继续,false或 panic 表示终止连接 - 每次写完必须显式
f.Flush(),否则 Go 的bufio.Writer可能积压几 KB 才发出去 - 不要在循环里反复调用
c.Header()—— Header 只能在第一次WriteHeader()前设置,重复调用无效
大文件流式下载别用 c.Stream(),改用 c.DataFromReader()
很多人误以为 c.Stream() 适合文件下载,其实它是通用流接口,缺乏文件场景的优化。对大文件,c.DataFromReader() 更合适:它绕过内存拷贝,支持 io.Reader + Content-Length + 自动分块,且内置 Flush() 控制。
对比 c.File() 和 c.DataFromReader():
-
c.File():读整个文件进内存再发 → OOM 风险高,不适用于 >100MB 文件 -
c.Stream():需手动管理 reader、flush、error → 容易漏 flush 导致卡顿 -
c.DataFromReader():传入*os.File或io.ReadSeeker,自动按 32KB 分块、设Content-Length、调用Flush()→ 推荐用于所有生产级文件下载
示例:
func downloadHandler(c *gin.Context) {
file, err := os.Open("./big-report.pdf")
if err != nil {
c.AbortWithStatus(http.StatusNotFound)
return
}
defer file.Close()
stat, _ := file.Stat()
c.Header("Content-Type", "application/pdf")
c.Header("Content-Disposition", "attachment; filename=report.pdf")
c.DataFromReader(http.StatusOK, stat.Size(), "application/pdf", file, nil)
}
Nginx 和客户端侧的缓冲干扰必须绕过
即使 Gin 层 flush 正确,Nginx、CDN 或浏览器自身也会缓冲响应。这是流式传输失败最常见的外部原因。
- Nginx 需禁用 proxy_buffering:
proxy_buffering off;,并设置proxy_cache off;、proxy_http_version 1.1;、proxy_set_header Connection ''; - Chrome 对空行(
\n\n)后未立即跟新 data 的 SSE 连接会延迟解析,务必保证每条消息以data: ...\n\n结尾 - 移动端 Safari 会静默关闭空闲超过 30 秒的 SSE 连接,服务端需定期发
:keepalive\n\n注释帧维持心跳
这些不是 Gin 的问题,但不处理,再正确的 c.Stream() 也会在生产环境失效。



















