必须手动控制响应流并禁用gin自动关闭连接,设置content-type为text/event-stream、cache-control为no-cache、connection为keep-alive,调用writeheader后持续flush;用带超时的select监听缓冲channel,配合sync.map管理客户端资源,并适配nginx/cdn代理配置。

能用,但必须手动控制响应流、禁止自动关闭连接,否则前端收不到后续推送。
设置 Content-Type 和禁用 Gin 自动结束响应
Go 的 gin.Context 默认在 handler 返回时就写完响应并关闭连接,这和 SSE 要求的“长连接+持续流式输出”直接冲突。必须显式设置头,并阻止 Gin 自动结束:
- 调用
c.Writer.Header().Set("Content-Type", "text/event-stream") - 调用
c.Writer.Header().Set("Cache-Control", "no-cache") - 调用
c.Writer.Header().Set("Connection", "keep-alive") - 关键一步:执行
c.Writer.WriteHeader(http.StatusOK)—— 不写这行,Flush()会失败 - 之后不能再调用
c.JSON、c.String等封装方法,否则会二次写 header 或提前关闭连接
用 channel + for range 持续监听并推送
SSE 连接生命周期内,服务端要一直等待新消息、写入、刷新。不能用普通 for 循环空转(CPU 占满),也不能用无缓冲 channel 直接阻塞(导致连接卡死)。推荐方式是带超时的 select + 缓冲 channel:
- 为每个客户端分配一个
chan string,容量建议设为 1–10,避免内存堆积 - 在 handler 中启动 goroutine 写入 channel(比如从任务完成事件触发)
- 主循环用
select等待 channel 消息或设置 30s 超时,超时后发一个空注释fmt.Fprintf(w, ":keepalive\n\n")防止代理断连 - 每次写完必须调用
w.(http.Flusher).Flush(),否则数据卡在缓冲区不出去
客户端断开时如何清理资源
浏览器关闭标签页、网络中断、用户刷新页面都会让连接无声断开,但 Go 后端不会立刻感知。常见错误是 channel 泄漏、goroutine 堆积:
- 不要依赖
ctx.Request.Context().Done()来判断断连 —— 它在某些反向代理后不可靠 - 更稳妥的做法:每次
Flush()后检查w.Hijacked() == false && !w.Flushed(),如果为 true 说明底层连接已失效 - 把 client channel 存进
sync.Map,key 用c.ClientIP() + c.Request.UserAgent()或业务 ID(如userId) - 推送前先尝试 send 到 channel;若因接收方已退出导致 panic 或阻塞,就从 map 中删掉该 channel
反向代理(Nginx / CDN)下容易丢消息
Nginx 默认缓存响应体、限制超时、合并换行符,SSE 在它后面极易失效:
- Nginx 配置里必须加:
proxy_buffering off;、proxy_cache off;、proxy_read_timeout 300; - 确保
proxy_set_header Connection '';,否则 Connection: keep-alive 可能被覆盖 - 部分 CDN(如 Cloudflare)默认不支持 SSE,需开启 “Streaming” 或改用企业版
- 测试时绕过代理直连
localhost:8080,确认逻辑正确后再查代理配置
最常被忽略的是 flush 和超时配合 —— 少一次 Flush(),前端就卡住;超时太长,Nginx 先断;超时太短,频繁发 keepalive 增加无谓流量。实际部署前务必用 curl 模拟长连接,观察响应流是否连续、中断后能否自动重连。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











