必须设置Content-Type: text/event-stream、Cache-Control: no-cache、Connection: keep-alive三个响应头,每次写入data:后须立即调用Flush(),且事件格式严格要求冒号后空格、结尾双换行。

必须设置的三个响应头:Content-Type、Cache-Control、Connection
浏览器只认 Content-Type: text/event-stream,缺了它,EventSource 会永远卡在 connecting 状态;Cache-Control: no-cache 防止代理或浏览器缓存初始响应,导致后续事件不触发;Connection: keep-alive 是显式声明长连接,否则 Nginx 或 ALB 可能中途切断。这三个头一个都不能少,顺序无关,但必须在第一次写入前设好。
每次写完 data: 后必须调用 http.Flusher.Flush()
Go 的 http.ResponseWriter 默认带缓冲,不手动 Flush(),数据就一直卡在内存里——前端永远收不到。常见错误是只在 handler 结尾 defer w.(http.Flusher).Flush(),这只会刷一次,起不到流式效果。正确做法是:每写一条 data: xxx\n\n 后立刻调用 flusher.Flush()。注意要先断言:flusher, ok := w.(http.Flusher),!ok 时应返回错误。
事件格式容错极低:data: 后必须空格,结尾必须 \n\n
SSE 协议对换行和空格极其敏感:data:hello(冒号后没空格)、data: hello\n(少一个 \n)、data: hello\r\n\r\n(用了 Windows 换行符)都会导致整条事件被丢弃。推荐封装函数避免手误:
func sendEvent(w io.Writer, msg string) {
fmt.Fprintf(w, "data: %s\n\n", msg)
w.(http.Flusher).Flush()
}如果要发 JSON,先 json.Marshal,再拼进 data: 行,不要直接 fmt.Fprintf(w, "data: %v\n\n", obj)——结构体字段顺序和空格不可控。
goroutine 泄漏和连接保活是线上最大隐患
每个 SSE 连接对应一个长生命周期 goroutine,若不监听 r.Context().Done() 就退出,客户端关掉页面后 goroutine 仍挂着,几天就 OOM。同时,HTTP server 默认的 ReadTimeout 和 WriteTimeout 会静默关闭空闲连接,必须设为 0 或大幅延长;反向代理(如 Nginx)的 proxy_read_timeout 也得同步调大。心跳不是可选——每 15–30 秒发一条 : ping\n\n(注释行,客户端忽略),能有效穿透中间网络设备。


















