go 的 http.responsewriter 需手动保持长连接:设 content-type、cache-control、connection 头,每次写后立即 flush;禁用 handler context 超时,改用 background context;通过 event id 实现断线续推;用单写 channel 串行化并发推送。

Go 的 http.ResponseWriter 怎么保持长连接不关闭
Server-Sent Events(SSE)依赖 HTTP 长连接,而 Go 默认的 http.ResponseWriter 在 handler 返回后会自动关闭连接。不手动干预,连接秒断,前端收不到后续事件。
关键不是“怎么开启 SSE”,而是“怎么阻止 Go 把连接关掉”:
- 必须在写入前调用
responseWriter.Header().Set("Content-Type", "text/event-stream") - 必须设置
responseWriter.Header().Set("Cache-Control", "no-cache"),否则某些代理或浏览器会缓存响应并阻塞流 - 必须调用
responseWriter.Header().Set("Connection", "keep-alive"),显式告诉底层不要复用或关闭 - 每次写事件后,必须立刻调用
responseWriter.(http.Flusher).Flush()—— 这是核心动作,不 flush,数据卡在缓冲区,前端永远等不到
为什么 net/http 的 context.WithTimeout 会导致 SSE 断连
HTTP handler 的 context 默认受服务器全局超时控制(如 http.Server.ReadTimeout 或 WriteTimeout),一旦触发,连接被强制中断,且不会通知你的业务逻辑。
这不是 bug,是设计使然:Go 的 HTTP server 认为“一个请求不该跑太久”。但 SSE 本质就是“一个请求持续很久”。所以:
立即学习“go语言免费学习笔记(深入)”;
- 绝不能依赖 handler 入参的
r.Context()做长期定时或等待 —— 它可能随时被 cancel - 需要自己新建一个不依赖 request 生命周期的 context,比如
context.Background()或带自定义 cancel 的 context - 若需感知客户端断开,应监听
responseWriter.(http.CloseNotifier).CloseNotify()(仅 Go 1.8–1.21;Go 1.22+ 已移除,改用responseWriter.(http.Hijacker)或检查write是否返回io.ErrClosedPipe)
前端用 EventSource 接收时反复重连怎么办
不是后端没发,是前端默认策略太激进:只要连接中断(哪怕只是网络抖动),EventSource 会在 3 秒后无脑重试,且不带任何标识,后端无法区分是新连接还是续连。
解决重连混乱的关键在于协议层对齐:
- 后端每次发送事件必须包含
id:字段,例如id: 12345,前端会自动记住最新 id - 前端发起请求时带上上次断连的 id:
new EventSource("/notify?last-event-id=12345")(需自行拼 query) - 后端收到
last-event-id参数后,应从该 id 对应位置继续推送(比如查数据库 > id 的记录,或从内存队列中跳过已发项) - 避免在 event 数据里用换行符
—— 会提前截断,导致解析失败;敏感内容先strings.ReplaceAll(data, " ", "\n")
并发推送时怎么避免多个 goroutine 写同一个 http.ResponseWriter
常见错误是:一个连接对应一个 handler,但业务侧有多个地方(比如订单服务、消息服务)都想往这个连接推通知,直接并发 Write() 会 panic:“write on closed connection” 或 “concurrent write to response body”。
根本解法是引入单写通道:
- handler 启动时,创建一个
chan string(或结构体),只供业务 goroutine 发送事件字符串 - handler 内部起一个专属 goroutine,循环读该 channel,并负责所有
Write()和Flush() - 连接关闭时,close channel 并 return,确保写 goroutine 退出,避免向已关闭 channel send 导致 panic
- 注意:不要用
sync.Mutex锁住Write()—— flush 不及时 + 锁竞争会让延迟飙升,失去实时性
真正难的从来不是“怎么发第一条消息”,而是“怎么在用户挂了 2 分钟又回来时,不丢通知、不重复推、不卡死连接”。这些细节藏在 flush 时机、id 对齐、channel 生命周期和上下文隔离里。










