登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go 写 SSE 接口为什么浏览器收不到消息:EventSource、Flush 和超时怎么排查

来源:17golang原创

时间:2026-07-07 16:05:26 333浏览 收藏

Go 写 SSE 接口时,浏览器 EventSource 能连上但迟迟收不到消息,最常见的原因不是浏览器“不支持”,而是服务端把数据写进响应后没有及时刷出去,或者中间的 Nginx、网关、压缩层把流式响应缓冲住了。排查这类问题,先看响应头是不是 text/event-stream,再看每条消息是不是以空行结尾,最后确认 Go 代码里拿到了 http.Flusher 并在每次写入后调用 Flush()

要点速览

  • SSE 是浏览器原生的单向事件流,适合通知、进度、状态变更场景,不适合做双向实时聊天协议。
  • Go handler 写入 data: ...\n\n 后要调用 Flush(),否则浏览器可能一直处于 pending 收不到内容的状态。
  • 代理层要关闭响应缓冲,Nginx 常见检查点包括 X-Accel-Bufferingproxy_buffering
  • 生产环境还要处理客户端断开、心跳保活、超时和自动重连逻辑,不能只写一个无限循环就完事。

EventSource 能解决什么,不解决什么

SSE 的全名是 Server-Sent Events,浏览器通过 new EventSource("/events") 建立一条普通 HTTP 长连接,服务端持续往这条连接里推送文本格式的事件。它的优势是足够轻量:不用做 WebSocket 升级,不用自己处理复杂握手逻辑,浏览器原生还自带断线自动重连能力。

但它只支持服务端到浏览器的单向推送。像订单状态同步、后台任务进度展示、系统告警、日志尾部实时更新这类场景都非常适配。如果页面还需要频繁往服务端回传数据,用 WebSocket 或者普通 HTTP 接口会更顺手。

能力点 SSE 的表现 排查时看哪里
连接方式 普通 HTTP 长连接 Network 面板里对应请求是否处于 pending 状态
消息方向 仅服务端推送给浏览器 EventStream 面板是否能抓到新的 data 内容
消息格式 data: 内容\n\n 是否漏了结尾空行或者正确的 Content-Type 头
实时性 强依赖 Flush 操作和各层代理的缓冲策略 Go、Nginx、网关是否开启了小块内容缓冲

浏览器 pending,不代表服务端已经推送到了页面

很多人第一次调试 SSE,在 DevTools 里看到请求状态是 200,类型标记也显示为 eventsource,就默认服务端逻辑肯定没问题了。这时候先别急着关调试面板。SSE 请求本来就会长时间维持 pending 状态,真正需要确认的是 EventStream 标签页里有没有新的事件输出,以及请求的 Waterfall 视图里有没有持续收到新的响应片段。

Go SSE 接口写入后未 Flush 导致浏览器 EventSource pending 等待

如果 Go 服务端日志已经打印了“成功写入消息”的记录,但浏览器侧完全没收到对应事件,问题基本都落在三个地方:第一是消息格式不符合规范;第二是内容写进缓冲区之后没有手动刷出;第三是中间链路的代理组件把小块响应攒起来做批量发送了。不要只盯着前端 JavaScript 查问题,整条服务端响应链路都要同步排查。

最小 Go 示例:写完一条就刷一次

下面这个 handler 是用来排查 SSE 问题的最简版本,刻意精简了多余逻辑,方便把核心关键点拎出来看清楚。

package main

import (
    "fmt"
    "net/http"
    "time"
)

func events(w http.ResponseWriter, r *http.Request) {
    flusher, ok := w.(http.Flusher)
    if !ok {
        http.Error(w, "streaming unsupported", http.StatusInternalServerError)
        return
    }

    h := w.Header()
    h.Set("Content-Type", "text/event-stream")
    h.Set("Cache-Control", "no-cache")
    h.Set("Connection", "keep-alive")
    h.Set("X-Accel-Buffering", "no")

    ticker := time.NewTicker(2 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case 

这里有三个细节最容易被忽略。第一,Content-Type 要设置成 text/event-stream。第二,每条消息末尾必须带一个空行,也就是 \n\n。第三,每次写入内容之后都要调用 Flush(),让当前的响应片段立刻从 Go 进程送到 TCP 连接上。

Go SSE handler 调用 http.Flusher 后浏览器收到 EventSource 消息

兼容处理:代理缓冲和超时更容易藏问题

本地直连 Go 服务能正常收到消息,上线经过 Nginx 或网关之后推送就卡住不动了,这种情况非常常见。代理默认一般会为了整体吞吐量做缓冲,把零散的小块响应攒到一定大小再统一发送,浏览器端就会表现出长时间收不到内容的状态。

如果你用 Nginx 做反向代理,可以先对照这些配置思路调整:

location /events {
    proxy_pass http://go_backend;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_buffering off;
    proxy_read_timeout 1h;
}

Go 侧加上 X-Accel-Buffering: no 也能起到辅助作用,但不要只靠响应头来解决缓冲问题。真实生产环境里还有负载均衡、CDN、API 网关、压缩中间件等多层组件,它们都可能修改流式响应的默认行为。排查的时候建议按“浏览器直连 Go 服务、经过 Nginx 转发、走完整网关链路”三步拆分验证,不要把所有层级混在一起盲目猜问题。

生产环境别忘了心跳和断开处理

SSE 不是随便写个死循环就能跑稳的。浏览器切换网络、用户关闭页面、代理触发空闲超时,都会悄无声息地断开连接。Go 代码里要监听 r.Context().Done() 事件,及时终止处理循环,避免无用的 goroutine 和 ticker 留在后台空转。

如果业务本身的事件推送频次很低,可以定期发一条注释型的心跳消息:

fmt.Fprint(w, ": ping\n\n")
flusher.Flush()

这行内容不会触发浏览器的 message 回调,但能维持连接活跃,也能帮你快速验证整条推送链路是否还能正常传输小块内容。心跳间隔不要随便设置,最好结合代理空闲超时阈值、服务端连接承载量和页面实际业务需求共同确定。

相关问题

SSE 和 WebSocket 应该怎么选?

页面只需要接收服务端下发通知的场景,优先考虑 SSE;需要双向高频通信的场景,再考虑 WebSocket。SSE 的优势是接入成本低、浏览器自带自动重连、普通 HTTP 链路调试起来更简单。

EventSource 收到 200 但没有 message,是前端问题吗?

不一定。200 状态只能说明连接成功建立,不能代表消息已经传输到页面。要继续排查响应格式、Flush()、代理缓冲配置和 EventStream 面板的实际输出内容。

SSE 可以传 JSON 吗?

可以。把 JSON 字符串放在 data: 后面即可,前端在 onmessage 回调里用 JSON.parse(event.data) 解析。注意每条事件末尾仍然需要用空行做结束标记。

Go 里不调用 Flush 会怎样?

写入的数据可能暂时留在进程的输出缓冲区里,浏览器看起来就会一直等待。某些情况下缓冲区写满之后内容也会被自动送出,但这不是实时推送场景应该依赖的行为。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>