Gin跑SSE必须绕过默认中间件、手动flush、禁用超时、配平代理层,漏任一环节EventSource即卡在connecting状态;因gzip篡改Content-Type、CORS提前WriteHeader、logger阻塞Flush,需用gin.New()建干净引擎,类型断言http.Flusher后逐条按格式写入并检查错误,同时配置Nginx/Cloudflare/ALB超时及每25秒发心跳。

直接说结论:Gin 跑 SSE 不是“加个路由就能用”,而是必须绕过默认中间件、手动 flush、禁用超时、配平代理层——漏掉任一环节,EventSource 就卡在 connecting 状态,且前端不报错、后端不报错、日志里也看不出问题。
为什么 Gin 默认路由会静默破坏 SSE
Gin 的 Default() 实例自带 gzip、logger、recovery 和(可能)CORS 中间件,它们会在你不知情时篡改关键响应头或阻塞刷新:
-
gzip中间件会重写Content-Type为text/plain或添加Content-Encoding: gzip,浏览器直接拒绝解析text/event-stream -
CORS中间件若提前调用WriteHeader(),你后续设置的Cache-Control或Connection头会被忽略 -
logger中间件在 handler 返回后才写日志,但 SSE 要求连接长期存活,它可能阻塞Flush()调用
正确做法是:不用 r.GET(),改用无中间件路由:
- Gin v1.9+:用
r.NoRoute()或新建一个干净引擎gin.New()专跑 SSE - 别在全局中间件里统一设头——
c.Header()必须在w.WriteHeader()前执行,且不能被覆盖
如何安全写入并立即 flush 每条消息
写完 data: 不等于客户端收到了。Go 的 ResponseWriter 默认带缓冲,不 Flush() 就永远卡在服务端内存里。
立即学习“go语言免费学习笔记(深入)”;
- 先做类型断言:
flusher, ok := w.(http.Flusher);失败就返回http.StatusInternalServerError - 每条消息严格按格式:
fmt.Fprintf(w, "data: %s\n\n", msg),结尾必须是两个换行符 - 写完立刻
flusher.Flush(),并检查 error:if err != nil && !errors.Is(err, net.ErrClosed) && !errors.Is(err, syscall.EPIPE) - 别用
json.Marshal()直接塞进data:字段——中文、双引号、换行会破坏格式;应先strings.ReplaceAll(msg, "\n", "\ndata: ")再拼
怎么防 Nginx / ALB / Cloudflare 断连
空闲超时是最大杀手。Go 服务没崩,用户却收不到推送,八成是代理层先动手了。
- Go 侧:
http.Server的WriteTimeout和ReadTimeout必须设为0(禁用),只保留ReadHeaderTimeout: 10 * time.Second - Nginx 配置里加:
proxy_read_timeout 300;、proxy_buffering off;、proxy_cache off; - Cloudflare 要进「Origin Rules」把 timeout 改成 300s;ALB 需关掉「Idle timeout」或设为 ≥300s
- 服务端每 25 秒发一次心跳:
fmt.Fprint(w, ":keepalive\n\n")(冒号开头的行是注释,客户端忽略,但能保活)
别信 c.Request.Context().Done() 判断断连
HTTP/1.1 下这个 channel 常滞后几秒甚至不触发,真实断连信号来自 Flush() 的 error。
- 每次
flusher.Flush()后必须检查 error,net.ErrClosed和syscall.EPIPE是客户端已关连接,其余错误要记录并退出 goroutine - 别用
defer flusher.Flush()——它只在 handler 返回时刷一次,起不到流式作用 - 用
json.Encoder替代手动拼字符串,避免引号转义错、JSON 格式非法导致前端触发error事件并自动重连
最易被忽略的是代理层配置和心跳间隔——写了 100 行 Go 代码,Nginx 里少加一行 proxy_read_timeout,整个 SSE 就形同虚设。


















