gin.Context.Stream是分块传输的首选方式,因其底层直接调用http.ResponseWriter.Flush并自动启用Transfer-Encoding: chunked(当未设Content-Length时),无需中间缓冲,适用于实时日志、SSE等流式场景。

为什么 gin.Context.Stream 是分块传输的首选方式
因为 Gin 的 Stream 方法底层直接调用 http.ResponseWriter 的 Flush,并自动设置 Transfer-Encoding: chunked(当未设 Content-Length 且响应未关闭时)。它不依赖中间缓冲,适合实时日志推送、大文件流式导出、SSE 等场景。
常见错误是手动写 Write 后忘记 Flush,导致浏览器一直等待;或提前写了 Content-Length,强制关闭 chunked 模式。
- 必须在首次
Write前不设置Content-Length(Gin 默认不设,但若你调过c.Header("Content-Length", ...)就会破坏流式) - 响应头中不要显式设置
Transfer-Encoding—— 让 net/http 自动处理 - 客户端需支持 chunked 编码(现代浏览器、
curl、fetch均支持;但某些旧版 Postman 可能缓存整块才解析)
如何用 c.Stream 实现逐行日志推送
这是最典型的分块使用场景:后端持续生成日志行,前端逐条渲染。关键在于传入的函数要能控制“每次推多少”,且不能阻塞太久。
func LogStreamHandler(c *gin.Context) {
c.Header("Content-Type", "text/plain; charset=utf-8")
c.Stream(func(w io.Writer) bool {
for i := 0; i < 10; i++ {
line := fmt.Sprintf("[%d] log entry %d\n", time.Now().Unix(), i)
_, _ = w.Write([]byte(line))
time.Sleep(500 * time.Millisecond) // 模拟延迟
// gin 自动在每次返回 true 前 Flush
}
return false // 结束流
})
}注意:Stream 的函数返回 true 表示继续,false 表示终止;每次返回前,Gin 都会调用 Flush。若函数 panic,连接会断开,但不会影响其他请求。
立即学习“go语言免费学习笔记(深入)”;
- 不要在
Stream函数里调用c.Abort或修改c.Writer状态 - 若需携带错误信息退出,建议先写错误行(如
"ERROR: xxx\n"),再返回false - 超时控制应由外部 context 管理(比如用
c.Request.Context().Done()检查中断)
c.SSEvent 和自定义 chunked 的区别在哪
c.SSEvent 是 Gin 对服务端事件(Server-Sent Events)的封装,本质仍是 chunked 传输,但协议更严格:每条消息必须以 data: 开头、双换行结尾,并可选 event:、id: 等字段。它专为浏览器 EventSource API 设计。
而裸用 Stream 更灵活,可输出纯文本、JSON 行(NDJSON)、自定义二进制分块等,但前端需用 fetch + ReadableStream 手动解析。
- 如果前端是
new EventSource("/logs")→ 用c.SSEvent - 如果前端用
fetch并处理流式 JSON → 用c.Stream+ 手动写{"msg":"..."}+ 换行 -
SSEvent会自动加Content-Type: text/event-stream和必要 header;Stream不会,需自行设置
生产环境容易忽略的三个细节
分块传输在开发时看似简单,上线后常因基础设施行为异常而中断。
- 反向代理(如 Nginx)默认缓冲响应:需配置
proxy_buffering off;和chunked_transfer_encoding on;,否则会攒满 4KB 才透传 - 某些云 WAF(如 Cloudflare)会拦截无
Content-Length的响应,或重写Transfer-Encoding;可临时用Content-Type: text/event-stream触发白名单 - Gin 默认的
gin.DefaultWriter在测试时可能被替换为非 flushable 的 writer(如bytes.Buffer),导致Stream无声失败 —— 单元测试务必用真实httptest.ResponseRecorder
真正卡住的往往不是 Go 代码,而是那一层没配对的 proxy_buffering 或 WAF 策略。


















