合法SSE需满足:响应头含Content-Type: text/event-stream、Cache-Control: no-cache、Connection: keep-alive,响应体每条以data:开头、结尾为\n\n,缺一则EventSource静默失败。

怎么判断后端返回的是合法 SSE 流
浏览器的 EventSource 会静默失败——连接状态卡在 connecting,控制台没报错,但收不到任何 message 事件。最直接的验证方式是用 curl 看原始响应:
curl -N http://localhost:8080/sse
必须看到连续输出、每条以 data: 开头、结尾是 \n\n(不是 \r\n\r\n),且首响应头含:
Content-Type: text/event-streamCache-Control: no-cacheConnection: keep-alive
缺任意一个,EventSource 就不会触发 open 状态。
为什么 io.ReadAll 会卡住,而 bufio.Scanner 能行
SSE 响应体不是完整 JSON,而是多块以 \n\n 分隔的文本片段,比如:
立即学习“go语言免费学习笔记(深入)”;
data: {"delta":{"content":"a"}}\n\n
data: {"delta":{"content":"b"}}\n\n
io.ReadAll 会一直等连接关闭才返回,但 SSE 连接本就不关;json.NewDecoder 会直接报 invalid character 'd' looking for beginning of value,因为开头是 data: 不是 {。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
正确做法是用 bufio.Scanner 配合自定义 SplitFunc 或直接按行扫:
- 每行用
strings.HasPrefix(line, "data: ")判断是否为数据行 - 跳过
: ping、空行、event:、id:等非 data 行 - 对匹配行调
strings.TrimPrefix(line, "data: ")后再json.Unmarshal
客户端断连时服务端 goroutine 怎么退出
Go 的 http.ResponseWriter 写失败时会返回 write: broken pipe 或 http: request canceled,但不主动 panic。若忽略错误继续写,goroutine 就永远卡在 Flush()。
必须每次写完都检查错误:
- 把
w.(http.Flusher)和io.Writer封装成带错误传播的写函数 - 在循环中用
if _, err := w.Write(...); err != nil { return }即刻退出 - 别依赖
context.Request().Done()——它在 HTTP/2 下可能延迟触发,不如直接捕获写错误可靠
真实项目里,90% 的 goroutine 泄漏都源于没检查这一个错误。
用 Gin 或原生 net/http 有啥实际区别
Gin 的 c.SSEvent() 只是语法糖,底层仍是调 fmt.Fprintf(c.Writer, "data: %s\n\n", v) + c.Writer.(http.Flusher).Flush()。但要注意:
- Gin 默认启用
gzip中间件,会破坏流式响应——必须手动禁用:r.Use(gin.Recovery(), gin.Logger()),不加gin.Gzip() - 原生
net/http更可控:你能直接断言http.Flusher,也更容易在http.Server层设IdleTimeout防代理掐断 - 两者都得自己处理并发广播,
sync.Map存连接 + 每连接独立 goroutine 是唯一安全模式
别被框架封装迷惑——SSE 的脆弱点永远在响应头、换行符、Flush 时机和错误处理上,不在路由层。

















