因为gin默认启用响应缓冲和http/1.1连接复用,且中间件(如gzip、cors、日志)会篡改关键响应头或提前writeheader,导致sse连接卡住或断连;必须绕过gin封装,直接操作底层http.responsewriter,设置正确header,每次写入后立即f.flush(),并禁用所有中间件。

为什么 gin.Context.Writer 直接写入会卡住或断连
因为 Gin 默认启用响应缓冲和 HTTP/1.1 连接复用机制,WriteHeader 调用后若未显式关闭连接或设置流式头,客户端可能等不到完整响应而超时;更关键的是,Gin 的 context.Abort() 和中间件生命周期会干扰长连接维持。
正确做法是绕过 Gin 的响应封装,直接操作底层 http.ResponseWriter,并手动控制 headers 和 flush 行为:
- 必须在首次写入前调用
w.Header().Set("Content-Type", "text/event-stream") - 必须禁用缓存:
w.Header().Set("Cache-Control", "no-cache") - 必须设置连接保持:
w.Header().Set("Connection", "keep-alive") - 每次发送数据后调用
w.(http.Flusher).Flush(),否则数据滞留在缓冲区 - 不能调用
c.JSON()、c.String()等 Gin 封装方法,它们会覆盖 header 或提前结束响应
如何安全地在 Goroutine 中向 SSE 连接写入数据
Gin 的 context 不是 goroutine 安全的,一旦 handler 函数返回,c.Request.Context() 可能被取消,且 c.Writer 会被回收。直接在子 goroutine 中使用原始 c.Writer 极易 panic 或写入失败。
推荐方案是把底层 http.ResponseWriter 和 http.Flusher 提取出来,配合 sync.Once 和 context.WithCancel 控制生命周期:
// 示例:提取可安全使用的 flusher
w := c.Writer
f, ok := w.(http.Flusher)
if !ok {
c.AbortWithStatus(http.StatusInternalServerError)
return
}
// 禁用 Gin 的 writer 封装,防止后续调用污染
c.Writer = &dummyWriter{} // 自定义空实现,避免 gin 写入
<p>// 启动 goroutine 发送事件
ctx, cancel := context.WithCancel(c.Request.Context())
go func() {
defer cancel()
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case </p><h3>
<code>curl</code> 测试 SSE 时看不到实时输出的常见原因</h3><p><code>curl</code> 默认启用缓冲,即使服务端已 flush,终端也不会立即显示。这不是代码问题,而是客户端行为。</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/gongju/2602" title="Gin框架 1.9.0"><img
src="https://img.php.cn/upload/manual/001/589/237/6a731f5b72949207.png" alt="Gin框架 1.9.0" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/gongju/2602" title="Gin框架 1.9.0" class="overflowclass">Gin框架 1.9.0</a>
<p class="overflowclass">Gin框架 1.9.0版本源码包下载,版本号 1.9.0,适合需要 sonic JSON 支持、路由修复和内容协商改进的 Go Web 开发场景。</p>
</div>
<a rel="nofollow" href="/xiazai/gongju/2602" title="Gin框架 1.9.0" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div><p>验证 SSE 是否真正工作,必须加 <code>-N</code>(禁用缓冲)和 <code>--no-buffer</code> 参数:</p>
- 错误命令:
curl http://localhost:8080/sse→ 看不到逐行输出 - 正确命令:
curl -N --no-buffer http://localhost:8080/sse - 浏览器访问时无此问题,但开发阶段建议用
curl -N快速验证流是否建立 - 若仍无输出,检查响应头是否含
Content-Type: text/event-stream,可用curl -I查看
多个客户端连接时如何避免 goroutine 泄漏
每个 SSE 请求都启一个 goroutine,若客户端异常断开(如关浏览器、网络中断),goroutine 可能永远阻塞在 write 或 select 上,导致内存泄漏。
必须结合 Request.Context().Done() 和写入时的 timeout 控制:
- 所有写操作需包裹在
select { case 中 - 使用
http.CloseNotify()已废弃,应依赖Request.Context()的 cancel 信号 - 在 handler 开头注册
defer cancel(),确保连接关闭时 goroutine 能退出 - 可选:用
sync.Map记录活跃连接 ID,配合心跳检测做主动清理
最易被忽略的一点:SSE 没有标准心跳协议,客户端静默超时后不会发 FIN 包,服务端只能靠 Context 超时或写入失败来感知断连 —— 所以 flush 失败时必须检查 err 并退出 goroutine。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










