Go原生net/http能跑通SSE,但需严格配置:显式设置Content-Type:text/event-stream和Cache-Control:no-cache;每次写data:后必须调用http.Flusher.Flush();禁用Read/WriteTimeout,调大IdleTimeout;Nginx proxy_read_timeout须与之对齐。

Go 原生 net/http 完全能跑通 SSE,但直接套用框架(比如 gin 或 echo)集成第三方 SSE 库时,90% 的失败不是因为库不兼容,而是中间件劫持了响应头、缓冲或连接生命周期——尤其是 gzip、logger、timeout 中间件会静默破坏流式响应。
为什么用 gin 写 SSE 会收不到 data:
gin 默认启用 gzip.Gzip() 和 gin.Logger(),这两个中间件在写响应前会接管 ResponseWriter,导致:
– w.Header().Set("Content-Type", "text/event-stream") 被 gzip 重写为 Content-Encoding: gzip,浏览器直接拒绝解析
– log.Println() 在 handler 循环里打日志,阻塞 Flush(),客户端卡在 pending 状态
– gin 的 Timeout 中间件会强制关闭空闲连接,比 Go 原生 http.Server 的 IdleTimeout 更激进
实操建议:
- 禁用所有中间件:启动路由时用
gin.New()而非gin.Default(),再手动注册必要中间件(且避开 SSE 路由) - SSE handler 单独挂到
gin.Engine.NoRoute()或用gin.RouterGroup.Use()排除 gzip/logger - 不要在 handler 里调
c.Writer,而应类型断言为http.ResponseWriter后操作,避免被 gin 封装层拦截
r3labs/sse/v2 库的 flush 和 context 控制陷阱
该库封装了流式写入逻辑,但默认行为与原生 SSE 要求存在隐含冲突:
– 它内部使用 bufio.Writer 缓冲,若未显式调 stream.Publish() 后跟 stream.Flush(),数据仍卡在缓冲区
– stream.Publish() 不检查客户端是否已断开,r.Context().Done() 在 HTTP/1.1 下常滞后,真实断连信号是 write: broken pipe 或 io.ErrClosedPipe
立即学习“go语言免费学习笔记(深入)”;
实操建议:
- 每次
stream.Publish()后立即调stream.Flush(),别依赖自动刷新 - 用
select监听r.Context().Done()和stream.Publish()的 error 返回值,后者才是第一手断连证据 - 别把
stream存全局变量——每个请求必须新建stream实例,否则并发 publish 会 panic
用原生 net/http 替代框架更稳?
是。尤其当你要控制超时、header、flush 时机这三处硬门槛时,框架抽象反而增加不可见路径。原生写法可控点明确:
-
http.Server的ReadTimeout和WriteTimeout必须设为0,否则连接会在几分钟后静默关闭 -
IdleTimeout推荐设为0或至少5 * time.Minute,防 Go 自身回收空闲连接 - 响应头必须在第一次
fmt.Fprintf()前设置,且顺序无关,但缺一不可:Content-Type、Cache-Control、Connection - 每次写完
data: ...\n\n后,必须做flusher, ok := w.(http.Flusher); flusher.Flush()类型断言并刷新
最易被忽略的一点:Nginx 代理层的 proxy_read_timeout 必须和 Go 的 IdleTimeout 对齐,否则它比你的服务先切断连接,前端看到的就是反复触发 error 事件后自动重连——这个链路里,Go 服务只是其中一环,不能只盯着代码改。


















