go的http.responsewriter需保持长连接以支持sse:设text/event-stream类型、禁用缓存、手动flush、严格遵循event/data/id格式、每连接独立goroutine+带缓冲channel、检测客户端断连并加心跳重连。

Go 的 http.ResponseWriter 必须保持长连接不关闭
Server-Sent Events 本质是 HTTP 长连接流式响应,服务端不能写完就关连接。Go 默认在 handler 返回后自动关闭连接,必须手动干预。
常见错误现象:net/http: request method or response status code does not allow body 或客户端只收到一次数据就断连。
- 用
w.Header().Set("Content-Type", "text/event-stream")声明 MIME 类型 - 禁用缓冲:
w.Header().Set("Cache-Control", "no-cache")和w.Header().Set("Connection", "keep-alive") - 调用
w.(http.Flusher).Flush()强制刷出响应头和每条事件,否则数据卡在缓冲区 - 不要用
fmt.Fprintf直接写入,改用io.WriteString+Flush()组合,避免隐式换行或编码干扰
event:、data:、id: 这些字段必须严格遵循 SSE 协议格式
浏览器的 EventSource 对解析非常敏感,任意拼写错误或空格缺失都会导致事件被静默丢弃。
使用场景:推送日志、实时计数、状态变更通知——所有需要按“事件类型+数据”区分处理的流式场景。
立即学习“go语言免费学习笔记(深入)”;
- 每条消息以空行分隔;
data:后必须跟一个换行,再写内容;多行data:要重复写前缀(data: line1\ndata: line2\n\n) -
event:字段决定addEventListener("xxx", ...)中的事件名,不写则默认为message -
id:用于断线重连时告诉服务端从哪条消息继续,但 Go 侧需自行维护序列号,协议本身不提供自动恢复 - 别漏掉末尾的双换行:
fmt.Fprint(w, "data: hello\n\n"); f.Flush()
并发推送时,每个连接要独立 goroutine + channel 控制
一个 handler 对应一个客户端连接,如果多个客户端共用同一个 channel,会互相覆盖或阻塞。
性能影响:goroutine 开销小(~2KB 栈),但 channel 缓冲区大小没设好会导致推送卡住或丢失。
- 为每个连接启动独立 goroutine:
go func(w http.ResponseWriter, r *http.Request) { ... }(w, r) - 用带缓冲的
chan string(如make(chan string, 16))接收业务层推送,防止生产者阻塞 - 在 goroutine 内循环读 channel,每次写完都
Flush();channel 关闭时主动 break 并 return - 注意:不要在 handler 里直接
time.Sleep模拟推送,会阻塞整个 HTTP 处理器
客户端断连后,Go 服务端不会立刻感知 write 失败
HTTP 连接断开后,w.Write() 可能仍成功返回(TCP FIN 还没传回),直到下次写或 flush 才报错,容易堆积 goroutine。
容易踩的坑:用 select 等待 channel + context.Done() 是不够的,必须检查底层连接状态。
- 每次
Flush()后检查:if f, ok := w.(http.Flusher); ok && !isClientConnected(r.Context()) { return } - 判断连接是否存活:Go 1.21+ 可用
r.Context().Err() == context.Canceled,但更可靠的是捕获write: broken pipe或use of closed network connection错误 - 把
http.ResponseWriter包装成支持中断的 writer,或用net.Conn.SetWriteDeadline配合超时检测 - 别依赖
defer清理资源——goroutine 可能永远卡在 channel receive 上
真正难处理的是网络闪断和 NAT 超时,SSE 没有心跳机制,得自己加 retry: 和定时 ping 事件,否则用户切后台几分钟后回来就收不到更新了。










