Go中SSE响应必须设置Content-Type: text/event-stream、Cache-Control: no-cache、Connection: keep-alive,缺一不可;还需用http.Flusher.Flush()逐条刷新data:开头、\n\n结尾的数据块,并监听req.Context().Done()防goroutine泄漏。

Go 中 SSE 响应必须设置哪些 HTTP 头
SSE 要正常工作,客户端(浏览器)依赖特定响应头识别流式数据。缺一不可,否则 EventSource 会直接关闭连接或报错 Network Error。
-
Content-Type必须为text/event-stream,不能是application/json或其他类型 -
Cache-Control设为no-cache,防止中间代理缓存整个流 -
Connection设为keep-alive,确保连接不被服务端主动断开 - 建议加
X-Accel-Buffering: no(Nginx 场景下必需,否则 Nginx 会缓冲直到换行才转发)
如何用 net/http 正确写入 SSE 数据块
每个 SSE 数据块必须以 data: 开头,结尾用双换行(\n\n)。Go 的 http.ResponseWriter 默认带缓冲,直接 Write 可能不立即发送;需显式调用 Flush()。
- 每次写入格式:先写
data: {...}\n\n,再调用flusher.Flush() - 必须用
http.Flusher类型断言判断是否支持刷新:if f, ok := w.(http.Flusher); ok { ... } - 不要用
fmt.Fprintf拼接后一次性写入——这会破坏流式节奏,且无法及时Flush - 若写入过程中发生错误(如客户端断开),
Write或Flush会返回非 nil error,应立即退出循环
data := map[string]interface{}{"id": 123, "msg": "hello"}
jsonBytes, _ := json.Marshal(data)
fmt.Fprintf(w, "data: %s\n\n", jsonBytes)
f.Flush()如何避免 Goroutine 泄漏和连接超时问题
HTTP handler 是阻塞的,但 SSE 长连接容易因客户端意外断开、网络中断或服务重启导致 goroutine 残留。不能只靠 defer 关闭 channel 或清理资源。
- 监听
req.Context().Done(),一旦触发(如客户端关闭连接),立刻退出写入循环 - 不要在 handler 内启动无管控的 goroutine 向 response 写数据——这会让 handler 提前返回,但后台 goroutine 仍在运行,导致泄漏
- 若需定时推送(如每 5 秒发一次),用
time.Ticker+select监听 context 和 ticker,而非time.Sleep - 注意默认 HTTP server 的
IdleTimeout(Go 1.8+ 默认 60s),长连接需显式加大,例如&http.Server{IdleTimeout: 5 * time.Minute}
为什么用 http.ServeMux 写 SSE 容易出错
http.ServeMux 本身没问题,但它的默认行为对 SSE 不友好:它会在 handler 返回后自动调用 CloseNotify()(已废弃)并可能提前关闭底层连接,尤其在使用反向代理(如 Nginx、Cloudflare)时更明显。
- 不要依赖
http.CloseNotify()判断连接状态——它在 Go 1.12+ 已被标记为 deprecated,且在某些 TLS/代理环境下始终返回空 channel - 改用
req.Context().Done()唯一可靠信号 - 如果用了第三方路由库(如 chi、gorilla/mux),确认其中间件未重写 response header 或包装 writer 导致
http.Flusher断言失败 - 本地测试可用
curl -N http://localhost:8080/sse,观察是否持续输出;浏览器中打开 DevTools → Network → 查看响应头和帧内容
真正麻烦的不是写出第一个 data 块,而是让连接稳住 5 分钟以上还不出错——context 控制、flush 时机、反向代理配置,三者缺一都会让 SSE 在真实环境里“偶发失效”。

















