Go实现SSE的核心难点是HTTP连接生命周期管理:必须禁用超时、显式设置Content-Type和Cache-Control响应头、每次写data:后调用Flush(),并用独立goroutine监听channel推送事件。

Go 实现 SSE 的核心难点不在协议本身,而在 HTTP 连接生命周期管理——http.ResponseWriter 默认会在 handler 返回时关闭连接,而 SSE 要求连接长期存活且数据逐条刷出。不手动干预,90% 的“只推一次就断”问题都源于此。
为什么客户端只收到第一条 data 就断连?
根本原因是 Go 的 HTTP handler 执行完就自动关闭连接,而 SSE 必须让连接持续打开并不断写入。浏览器看到响应结束,就认为流已终止。
-
w.Header().Set("Content-Type", "text/event-stream")和w.Header().Set("Cache-Control", "no-cache")是必须的,但仅设置头部远远不够 - 必须显式调用
w.(http.Flusher).Flush(),否则数据卡在缓冲区,不会真正发到客户端 - 不能依赖
fmt.Fprintf自动换行;data:后必须紧跟一个\n,整条事件以\n\n结尾,缺一不可 - 常见错误写法:
fmt.Fprintf(w, "data: %s", msg)—— 漏掉换行和双换行,EventSource 直接静默丢弃
如何安全地向每个连接推送消息而不阻塞?
每个 SSE 连接必须独占一个 goroutine,且需配合带缓冲的 channel 控制生产/消费节奏。共用 channel 或无缓冲 channel 会导致推送卡死或消息覆盖。
- 在 handler 内启动独立 goroutine:
go func(w http.ResponseWriter, r *http.Request) { ... }(w, r) - channel 建议设缓冲(如
make(chan string, 16)),避免业务层因推送慢而阻塞 - goroutine 内用
for msg := range ch循环读取,每次写完立即Flush();channel 关闭后主动return - 切勿在 handler 主 goroutine 中
time.Sleep或长时间阻塞,会拖垮整个 HTTP server
event:、id:、retry: 字段怎么用才不被浏览器忽略?
浏览器 EventSource 对字段拼写、空格、换行极其敏感。任意格式偏差都会导致字段失效,甚至整条事件被跳过。
立即学习“go语言免费学习笔记(深入)”;
-
event:后必须跟一个空格,再写事件名(如event: update),客户端用addEventListener("update", ...)接收 -
id:用于断线重连时标记位置,但 Go 侧需自行维护递增序列号,协议不提供自动同步 -
retry:单位是毫秒,如retry: 3000,告诉浏览器断连后等 3 秒重试;注意它只影响客户端行为,服务端无感知 - 多行
data:必须每行都写前缀:data: line1\ndata: line2\n\n,不能合并成data: line1\nline2\n\n
用第三方库(如 r3labs/sse)能省掉哪些坑?
它封装了连接管理、多 stream 隔离、client 列表维护、心跳检测等底层逻辑,但代价是引入额外抽象层和运行时开销。
- 直接用
github.com/r3labs/sse/v2可跳过手动 flush、goroutine 分配、断连检测等细节 - 适合快速验证或中低并发场景;高吞吐日志推送类服务,原生实现更可控
- 注意其
server.CreateStream("xxx")创建的是命名流,客户端需指定new EventSource("/events?stream=xxx")才能订阅 - 它默认不处理 client 断连后的 channel 清理,仍需监听
http.CloseNotify()或检查Write错误来主动退出 goroutine
最易被忽略的一点:SSE 连接断开时,Go 服务端不会立刻收到通知。必须在每次 Write 后检查 err,或定期尝试 WriteHeader(204) 探活,否则 goroutine 和 channel 会持续泄漏。


















