因为 Gin 默认启用 Content-Length 自动计算和响应体缓冲,而 SSE 要求长连接、分块传输(text/event-stream + chunked),c.Writer.Write() 若未配合正确 header 和显式 Flush 会导致连接提前关闭或后续写入失效。

为什么 gin.Context.Writer 直接写入会断开 SSE 连接
因为 Gin 默认启用 HTTP/1.1 的 Content-Length 自动计算和响应体缓冲,而 SSE 要求连接长期保持、分块写入(text/event-stream + 持久 Transfer-Encoding: chunked)。一旦 c.Writer.Write() 触发了 flush,但 Gin 在后续未显式禁用缓冲或设置正确 header,底层 http.ResponseWriter 可能提前关闭连接或忽略后续 write。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 必须在首次响应前调用
c.Writer.Header().Set("Content-Type", "text/event-stream")和c.Writer.Header().Set("Cache-Control", "no-cache") - 必须调用
c.Writer.Header().Set("Connection", "keep-alive"),否则某些反向代理(如 Nginx)会主动断连 - 务必调用
c.Writer.Flush()每次写完一条 event 后——不能只靠fmt.Fprintln或io.WriteString就完事 - 避免使用
c.JSON()或c.String(),它们会覆盖 header 并强制结束响应
如何用 gin.HandlerFunc 正确实现 SSE 流式写入
Gin 本身不提供 ResponseWriter 的流式封装,需手动管理 writer 状态和心跳。关键不是“怎么注册路由”,而是“怎么防止 goroutine 泄漏 + 怎么让 client 不断连”。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
c.Stream(func(w io.Writer) bool { ... })是错误选择——它只适合单次流式输出,不支持长连接维持 - 应直接操作
c.Writer,并在 handler 内启一个 goroutine 处理业务事件,主 goroutine 负责监听c.Request.Context().Done()退出 - 每 15–30 秒写一次空 event(
fmt.Fprint(w, ":\n\n"))作为心跳,防止中间代理超时断连 - 写 event 格式必须严格:以
data:开头、末尾双换行,如fmt.Fprintf(w, "data: %s\n\n", msg);不要漏掉最后两个\n - 务必在 handler 结束前调用
c.Writer.Flush(),否则最后一段可能卡在缓冲区
反向代理(Nginx / Caddy)下 SSE 失败的典型配置坑
本地跑通不代表上线可用。90% 的线上 SSE 断连问题出在代理层吞掉了 Transfer-Encoding: chunked 或设置了过短的超时。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- Nginx 中必须显式开启流式支持:
proxy_buffering off;、proxy_cache off;、proxy_http_version 1.1;、proxy_set_header Connection ''; - Nginx 的
proxy_read_timeout至少设为300(5 分钟),否则默认 60 秒就 kill 连接 - Caddy v2 需在 route 中加
reverse_proxy的transport http块,并设keepalive 0s和tls_insecure_skip_verify(若后端是自签证书) - 确认代理没有重写
Content-Type或注入Content-Length—— 可用curl -v对比直连和代理后的 response header
客户端接收不到更新?检查 EventSource 的重连逻辑和错误处理
浏览器 EventSource 默认在连接失败后指数退避重试(最多约 3 分钟),但不会报错到 console,容易误判为“服务没发”。而且它对非 2xx 响应静默失败。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 服务端返回非 200(如 503)时,
EventSource会立即重连,但不会触发onerror回调——要靠onopen是否被反复触发来判断 - 在客户端加
eventSource.addEventListener('error', () => console.log('SSE error')),并检查eventSource.readyState是否长期为0 - 避免在服务端 handler 中 panic,Gin 默认 recover 会返回 500,导致客户端疯狂重连;建议用
defer+recover捕获并主动 close 连接 - 调试时用
curl -N http://localhost:8080/sse直接看原始流输出,比浏览器更可靠
真正难的不是写出第一个 data:,而是让连接在高并发、网络抖动、代理介入、客户端休眠等场景下仍稳定存活。SSE 的健壮性几乎全靠 header 控制、flush 时机和心跳节奏——这些细节 Gin 不替你做,得自己盯住每个 \n 和每次 Flush()。


















