本文详解如何在基于 eventsource 库的 go sse 服务中可靠检测客户端断开连接,避免 goroutine 泄漏;核心方案是利用 http.request.context().done() 通道监听请求生命周期终结事件。
本文详解如何在基于 eventsource 库的 go sse 服务中可靠检测客户端断开连接,避免 goroutine 泄漏;核心方案是利用 http.request.context().done() 通道监听请求生命周期终结事件。
在构建实时聊天室(如基于 Twitter 标签的流式消息系统)时,Server-Sent Events(SSE)是一种轻量、单向、兼容性良好的推送方案。然而,一个常见却易被忽视的问题是:当浏览器标签页关闭、网络中断或客户端主动断连时,服务端无法感知,导致发送 goroutine 持续运行、资源泄漏甚至引发 panic(如向已关闭的 HTTP 连接写入数据)。
Go 自 1.8 起已弃用 http.CloseNotifier 接口,官方推荐统一使用 Request.Context() —— 它会在客户端断连、请求超时或服务器主动取消时自动关闭其关联的 Done() channel。这是检测断连最标准、最健壮的方式。
以下是一个修复后的完整 ServeHTTP 实现,融合了上下文监听、优雅退出与错误防护:
func (sh StreamHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
es := eventsource.New(
&eventsource.Settings{
Timeout: 2 * time.Second,
CloseOnTimeout: true,
IdleTimeout: 2 * time.Second,
Gzip: true,
},
func(req *http.Request) [][]byte {
return [][]byte{
[]byte("X-Accel-Buffering: no"),
[]byte("Access-Control-Allow-Origin: *"),
}
},
)
// 启动 SSE 连接(非阻塞)
es.ServeHTTP(resp, req)
// 启动消息发送 goroutine,并监听断连信号
done := make(chan struct{})
go func() {
<-req.Context().Done() // 阻塞直到请求上下文取消(即客户端断开)
close(done)
}()
// 主发送循环:每秒推送一条消息,但每次发送前检查连接状态
var id int
for {
select {
case <-done:
// 客户端已断开,安全退出
log.Printf("Client disconnected, stopping stream for %s", req.RemoteAddr)
return
default:
id++
time.Sleep(1 * time.Second)
// SendEventMessage 内部会检查底层连接是否可用;
// 若已断开,通常会返回 error(取决于 eventsource 库版本),建议捕获处理
if err := es.SendEventMessage("blabla", "message", strconv.Itoa(id)); err != nil {
log.Printf("Failed to send SSE event: %v", err)
return
}
}
}
}✅ 关键要点说明:
- req.Context().Done() 是一个只读 channel,一旦关闭即表示请求终止(含断连、超时、服务端 cancel 等),无需额外轮询或心跳。
- 使用 select + default 模式可避免 time.Sleep 阻塞导致无法及时响应断连;若需更高实时性,可将 Sleep 移至 select 的 case <-time.After(...) 分支中。
- eventsource 库新版(v2+)通常会在底层写入失败时返回 error,务必检查 SendEventMessage 返回值,并在出错时主动退出 goroutine,防止无限重试。
- 切勿在 http.ResponseWriter 或 eventsource 对象上做同步阻塞操作(如未加 context 控制的 Write),否则可能阻塞整个 HTTP handler。
⚠️ 注意事项:
- 确保你的 eventsource 版本 ≥ v2.0.0(支持 Context 集成);旧版需手动升级或改用 github.com/r3labs/sse 等更活跃维护的替代库。
- 生产环境应添加日志追踪(如请求 ID、用户标识)、连接数监控及超时熔断机制,避免突发断连风暴拖垮服务。
- 若需支持多房间/多用户广播,建议将每个连接抽象为独立 StreamSession 结构体,内嵌 context.Context 和 sync.WaitGroup,便于统一管理生命周期。
通过将客户端生命周期与 Go 原生 Context 深度绑定,你不仅能精准感知断连,还能实现资源自动回收、goroutine 安全退出和系统级可观测性——这才是现代 Go Web 实时服务的正确打开方式。










