在 gin 中,中间件内调用 ctx.writer.flush() 会破坏 chunked 流式语义,因响应头可能已被提前写入,导致无法设置 transfer-encoding: chunked;可控的流式响应必须在 handler 内手动管理头部、分块写入与 flush。

中间件里直接 Flush 会破坏 chunked 流式语义
在 Gin 中,ctx.Writer.Flush() 看似能“推”出数据,但若在中间件中调用,极大概率导致响应头已发送、后续无法再写 Transfer-Encoding: chunked —— 因为 Gin 默认会在第一个 Write() 或 JSON() 调用时自动写入 Content-Length 头并关闭 chunked 通道。
常见错误现象:http: multiple response.WriteHeader calls 报错,或客户端收到不完整响应、解析卡死。
- 中间件不是流式输出的合适位置:它面向请求预处理/后置包装,而非响应体构造
- 一旦
ctx.Writer.Status()或任意Write()被触发(哪怕只是日志中间件读了r.Body),Gin 就可能提前锁定响应头 - 若必须在中间件注入流式逻辑(如统一添加 SSE 前缀),需确保所有 handler 都禁用自动 header 注入,并由中间件自己控制首次
Write和Flush
真正可控的分块传输必须在 Handler 内显式管理
分块传输的核心是:不设 Content-Length、只发 Transfer-Encoding: chunked、按需写块、手动 Flush。Gin 不提供开箱即用的 chunked 封装,必须绕过 c.JSON() / c.String() 等高层方法。
正确做法是直接操作 http.ResponseWriter,并禁用框架默认行为:
- 在 handler 开头调用
c.Status(200),但**不要**调用c.Header("Content-Type", ...)—— 改用c.Writer.Header().Set(...),避免被自动覆盖 - 手动写头:
c.Writer.Header().Set("Transfer-Encoding", "chunked"),且确保没设Content-Length(检查是否被其他中间件偷偷加了) - 用
c.Writer.Write([]byte{...})写每个块,格式为:"5\r\nHello\r\n"(十六进制长度 + CRLF + 数据 + CRLF) - 每写完一块,立刻
c.Writer.Flush();终结块为"0\r\n\r\n"
示例关键片段:
c.Writer.Header().Del("Content-Length")
c.Writer.Header().Set("Transfer-Encoding", "chunked")
c.Writer.Header().Set("Content-Type", "text/event-stream")
c.Status(200)
c.Writer.Write([]byte("6\r\nhello!\r\n"))
c.Writer.Flush()
c.Writer.Write([]byte("0\r\n\r\n"))
c.Writer.Flush()
中间件干扰流式传输的三个典型陷阱
Gin 的中间件链天然与流式响应存在冲突,尤其当涉及日志、压缩、CORS 或鉴权时:
-
gzip.Gzip()中间件:会缓冲全部响应体再压缩,彻底阻断流式——必须全局禁用,改用客户端自行解压或服务端分块后不压缩 - 日志中间件(如
gin.Logger()):默认读取c.Writer.Size(),触发内部 header 写入;应替换为不依赖Size()的轻量日志,或仅记录请求阶段 - 鉴权中间件读取
r.Body后未调用r.Body.Close()或未还原 body(如用io.NopCloser包装),会导致后续 handler 读不到 body,间接影响流式响应构造逻辑
验证方式:用 curl -v http://localhost:8080/stream 观察响应头,确认只有 Transfer-Encoding: chunked,无 Content-Length,且响应体分段到达。
大路径文本流场景下,别依赖中间件做语义分块
对日志尾部、SQL 流式结果、SSE 推送等“大路径文本流”,分块粒度必须匹配业务单元(如一行日志、一条 SQL 记录),而非固定字节数。中间件无法感知 handler 内部的数据生成节奏,强行在中间件切分只会导致块断裂、JSON 解析失败或时间戳错乱。
可行路径只有两条:
- Handler 自己生成数据并逐块写入(推荐):用
bufio.NewWriter(c.Writer)提升小块写入效率,配合time.Ticker控制推送节奏 - 用
c.Stream()(Gin 内置流式接口):它封装了 flush 逻辑,但要求传入函数返回bool控制是否继续,适合简单循环场景;注意它仍会自动设Content-Type,需提前清除Content-Length
最易被忽略的一点:Nginx 等反向代理默认缓冲 chunked 响应。若你本地测试正常、上线后流式失效,大概率是 Nginx 的 proxy_buffering on 或 proxy_cache 在作祟——必须显式关掉,并加 proxy_http_version 1.1 和 proxy_set_header Connection ''。











