Go实现SSE推送核心是设对响应头、每次写完立刻flush、禁用超时;漏任一环节,EventSource即卡在connecting状态。

Go 实现 SSE 推送,核心就三件事:设对响应头、每次写完立刻 flush、禁用超时。漏掉任意一条,前端 EventSource 就卡在 connecting 状态。
为什么写了 fmt.Fprintf(w, "data: hello\n\n") 却收不到事件?
因为 Go 的 http.ResponseWriter 默认带 bufio.Writer 缓冲,fmt.Fprintf 只是把字节塞进内存缓冲区,不主动刷出,客户端永远等不到完整的 \n\n 结尾。
- 必须显式做类型断言:
if f, ok := w.(http.Flusher); !ok { http.Error(w, "streaming unsupported", http.StatusInternalServerError); return } - 写完每条事件后必须调
f.Flush(),不能只依赖defer f.Flush()(它只在 handler 返回时执行一次) - 如果用了中间件(比如 gzip),可能破坏流式响应——
Content-Encoding会被重写,导致浏览器解析失败
text/event-stream 响应头缺一不可的三项配置
浏览器只认这三个头,少一个都可能让 EventSource 失效:
-
w.Header().Set("Content-Type", "text/event-stream"):没这个,状态永远停在connecting -
w.Header().Set("Cache-Control", "no-cache"):Chrome/Nginx 会缓存初始响应,后续事件不触发message -
w.Header().Set("Connection", "keep-alive"):防某些反向代理提前关闭空闲连接
注意:这些头必须在 w.WriteHeader() 之前设置,否则可能被忽略;也别在中间件里统一设——gzip 中间件可能覆盖 Content-Type。
立即学习“go语言免费学习笔记(深入)”;
如何安全地检测客户端断连并清理 goroutine?
Go 没有直接暴露连接状态,但真实断连信号藏在 Write 或 Flush 的 error 里:
- 每次
f.Flush()后检查 error:if err != nil && !errors.Is(err, syscall.EPIPE) && !errors.Is(err, net.ErrClosed) { /* 其他错误要处理 */ } - 不要只依赖
r.Context().Done()—— HTTP/1.1 下它常滞后,write: broken pipe才是第一手断连证据 - 服务端
http.Server必须设ReadTimeout和WriteTimeout为0,否则几分钟后连接被静默关闭;改用ReadHeaderTimeout控制请求头读取(如 10 秒) - 如果前面有 Nginx,
proxy_read_timeout也得同步调大,否则它比 Go 先断
用 json.Encoder 替代手动拼 data: 的实际好处
手动拼 JSON 容易出错:引号没转义、逗号漏写、结构不合法——前端收到半截 JSON 就会触发 error 事件并自动重连。
- 用
enc := json.NewEncoder(w)初始化一次,复用它调enc.Encode(v) - 每次
Encode()后紧跟f.Flush(),保证每个 JSON 值独立可解析 - 如果发的是 SSE 格式,别混用:
json.Encoder输出纯 JSON,而 SSE 要求data: {...}\n\n,此时仍需手动写前缀和换行 - 想发多行 JSON(NDJSON),用
enc.Encode()+f.Flush()是最稳的组合
真正容易被忽略的是:SSE 不是“只要能连上就行”,而是对响应头、缓冲控制、超时配置这三处有硬性门槛。线上跑通的 SSE,90% 的问题都出在这三个地方,而不是逻辑本身。


















