go原生net/http完全支持sse,需设content-type: text/event-stream、cache-control: no-cache、connection: keep-alive及x-accel-buffering: no,每次写事件后必须显式flush,且用独立goroutine+channel串行推送,结尾严格双换行,并检查write(nil)判断断连。

Go 原生 net/http 完全支持 Server-Sent Events(SSE),不需要第三方库,但必须手动处理响应头、连接保持和事件格式——稍有疏忽就会导致客户端收不到事件或连接频繁断开。
如何正确设置 SSE 响应头与缓冲
SSE 要求服务端维持长连接,并禁用响应缓冲。Go 的 http.ResponseWriter 默认会缓冲输出,必须显式关闭;同时需设置关键响应头。
-
Content-Type必须为text/event-stream,且不能带 charset(如text/event-stream; charset=utf-8在部分浏览器中会失败) - 必须设置
Cache-Control: no-cache和Connection: keep-alive - 调用
rw.Header().Set()后,需立即调用rw.(http.Flusher).Flush()确保响应头发出;否则客户端可能卡在 pending 状态 - 推荐在 handler 开头就做:
rw.Header().Set("Content-Type", "text/event-stream") rw.Header().Set("Cache-Control", "no-cache") rw.Header().Set("Connection", "keep-alive") rw.Header().Set("X-Accel-Buffering", "no") // 防 Nginx 缓冲 if f, ok := rw.(http.Flusher); ok { f.Flush() }
如何安全地向长期连接写入事件数据
写入事件时不能直接用 fmt.Fprintf 或 io.WriteString 后不 flush,否则数据滞留在 Go 的 bufio.Writer 中;更危险的是并发写入同一连接(比如多个 goroutine 往同一个 http.ResponseWriter 写)会导致 panic。
- 每个 SSE 连接应绑定一个专属 goroutine,用 channel 接收待推送消息,再串行写入
- 每条事件必须以
data:开头,结尾用双换行(\n\n),可选加id:和event: - 写完每条事件后必须调用
flusher.Flush(),否则客户端收不到 - 示例片段:
for msg := range ch { fmt.Fprintf(rw, "id: %s\n", msg.ID) fmt.Fprintf(rw, "event: %s\n", msg.Type) fmt.Fprintf(rw, "data: %s\n\n", msg.Payload) if f, ok := rw.(http.Flusher); ok { f.Flush() } // 检查连接是否断开(客户端关闭时 Write 会返回 error) if _, err := rw.Write(nil); err != nil { break } }
如何检测客户端断连并清理资源
HTTP 连接断开时,Go 不会自动通知 handler,Write 或 Flush 可能静默失败或返回 broken pipe 类错误——若不检查,goroutine 会持续运行并泄漏内存。
- 每次写入后建议执行一次空写
rw.Write(nil),它会触发底层连接状态检查,返回http.ErrHandlerTimeout或io.EOF等错误 - 也可监听
context.Request.Context().Done(),但注意:客户端关闭连接时该 channel 不一定立即关闭(受 TCP linger、代理等影响) - 务必在循环退出后关闭 channel 并 return,避免 goroutine 泄漏
- 不要依赖
defer关闭连接——连接已由 http.Server 管理,你只需停止向它写入
最易被忽略的点是:Nginx 默认会缓冲 SSE 响应,即使你代码里 flush 了,用户也收不到实时事件;必须在 Nginx 配置中显式添加 proxy_buffering off; 和 proxy_cache off;,否则所有努力都白费。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











