根本原因是Gin默认中间件静默破坏SSE:gzip篡改Content-Type、CORS提前WriteHeader覆盖关键头、日志中间件阻塞Flush;必须用无中间件路由、每次消息后立即f.Flush()并检查error,配合心跳和超时控制防断连。

为什么 Gin 的 SSE 路由总卡在 connecting 状态
根本原因不是代码写错,而是 Gin 默认中间件在你不知情时篡改了关键响应头或提前触发了 WriteHeader。比如:gzip 中间件会把 Content-Type: text/event-stream 悄悄替换成 Content-Encoding: gzip,浏览器直接拒收;CORS 中间件可能在你调 c.Header() 前就写了状态码,导致 Cache-Control 和 Connection 失效;日志中间件则可能阻塞 Flush() 调用。
必须绕过所有中间件:用 r.NoRoute()、r.Any("/sse", handler),或者更稳妥地新建一个独立 gin.Engine 实例专跑 SSE 路由。别试图给现有路由加 gin.BasicAuth() 或其他中间件——它们几乎都会破坏流式响应。
如何正确设置响应头和写入 SSE 数据
SSE 不是“设完头就能发”,每条消息都必须严格遵循格式,且每次写入后立刻 Flush(),否则前端永远收不到。
-
Content-Type: text/event-stream、Cache-Control: no-cache、Connection: keep-alive这三个头缺一不可,顺序无关,但必须在第一次写数据前设置 - 每条消息以
data:开头,结尾必须是两个换行符\n\n(不是\r\n\r\n),例如:data: hello\n\n - 中文、双引号、换行符必须转义:用
strings.ReplaceAll(msg, "\n", "\ndata: ")替换后再拼进data:字段 - 注释行以
:开头,如:heartbeat\n\n,客户端忽略但能防代理超时
怎么避免 Nginx / ALB / Go 自身静默断连
真实部署中,90% 的断连不是代码问题,而是中间件或服务端超时策略不匹配。
- Go 侧:启动
http.Server时显式设WriteTimeout: 0(禁用写超时),否则默认 2 分钟就会关连接 - Nginx 侧:
proxy_read_timeout至少设为 300(5 分钟),同时加proxy_buffering off;和proxy_cache off; - 必须主动心跳:每 25–45 秒发一次
:heartbeat\n\n,不能只靠空闲保活 - 别只依赖
c.Request.Context().Done()判断断连——它可能延迟数秒才触发,应结合Flush()返回的 error 判断:errors.Is(err, net.ErrClosed)或errors.Is(err, syscall.EPIPE)
如何实现多客户端广播而非单次流式推送
如果需求是“用户触发动作后,向指定客户端群发通知”,就不能用无限 for 循环 + ticker,而要维护连接池。
- 用
sync.Map存储roomID → []chan string,每个客户端连接对应一个独立chan - 连接建立时,生成专属 channel 并注册到 map;断开时从 map 中清理(注意 defer 里不能直接删 map 元素,需另起 goroutine 或加锁)
- 触发广播时,遍历目标 room 的所有 channel,
select { case ch 避免阻塞 - 客户端 channel 读取端必须用
for msg := range ch,不能用select+default轮询,否则会漏消息
最易被忽略的是 channel 关闭时机:不是连接断开就立刻 close,而是要在确认写入失败且 error 可判为断连后,再 close 对应 channel,否则广播时会 panic。


















