当前位置:首页 > 文章列表 > Golang > Go教程 > Go SSE 推送的 Flush 触发与断开处理

Go SSE 推送的 Flush 触发与断开处理

来源:17golang原创 2026-10-15 08:20:13 0浏览 收藏

Go 写 SSE(Server-Sent Events)时,循环调用 Write 并不等于客户端马上收到事件。稳定的处理方式是:先声明 text/event-stream,每个事件写成以空行结尾的 SSE 帧,再用 http.NewResponseController(w).Flush() 把当前缓冲提交出去;同时用 ticker 发送轻量心跳,并监听 r.Context().Done() 感知客户端断开。

Flush 解决的是服务端把已写入数据交给 HTTP 层的问题,不能保证所有代理立即转发;心跳解决空闲期间没有字节流的问题,Context 取消则负责让事件生产和 ticker 随连接结束。
  • 响应头决定客户端按事件流解释内容,事件帧必须用空行分隔。
  • ResponseController.Flush 比直接断言 http.Flusher 更适合需要处理错误、兼容 ResponseWriter 包装器的 Handler。
  • 心跳可以使用 SSE 注释行,既能产生字节流,又不会触发业务事件。
  • 客户端离开后,Request.Context 会被取消,生产者、ticker 和 Handler 都应沿着这条信号退出。

先把 SSE 响应边界写清楚

SSE 响应的主体不是任意文本,而是由字段行组成的事件帧。最小的业务事件通常写成 data: 内容\n\n,其中最后的空行告诉客户端当前事件已经结束。响应头至少要让客户端知道内容类型,并避免缓存层把多个事件长时间合并。

Handler 不能在写出第一个事件后立即返回,因为返回意味着 ServeHTTP 已经结束,后续继续使用 ResponseWriter 就越过了它的生命周期。持续推送的循环必须留在 Handler 内,并且有明确的退出分支。

package sse

import (
    "fmt"
    "net/http"
)

func writeEvent(w http.ResponseWriter, name, data string) error {
    // event 和 data 都是 SSE 字段;空行表示这一帧已经结束。
    if name != "" {
        if _, err := fmt.Fprintf(w, "event: %s\n", name); err != nil {
            return err
        }
    }
    if _, err := fmt.Fprintf(w, "data: %s\n\n", data); err != nil {
        return err
    }

    // Flush 只提交已经写入的响应数据,不负责跨代理保证即时可见。
    return http.NewResponseController(w).Flush()
}

func initialHandler(w http.ResponseWriter, r *http.Request) {
    // 这些头必须在第一次 Write 之前设置,避免响应状态已经发出后再修改。
    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("X-Content-Type-Options", "nosniff")

    if err := writeEvent(w, "ready", "连接已建立"); err != nil {
        // 客户端已离开时,记录错误并返回即可,不要继续写响应。
        return
    }
}

这里使用 ResponseController 的好处是它可以沿着包装器的 Unwrap 找到底层响应能力,并把不支持 Flush 的情况作为错误返回。Go 官方文档也提醒,标准 HTTP/1.x 和 HTTP/2 响应通常支持 Flusher,但 ResponseWriter 包装器未必支持,因此不要假设所有中间件都保留了该能力。

Go SSE 从响应头、事件写入到 ResponseController Flush 和客户端事件流的静态关系图
图1:SSE 事件从 Handler 写入后,经 ResponseController.Flush 提交到 HTTP 层;代理缓冲仍可能影响客户端可见时间。

Flush 要放在每个事件帧之后

如果只调用 Write,数据可能仍停留在服务端或中间层的缓冲中。对于一个需要尽快到达客户端的事件,顺序应保持为“写字段、补空行、Flush”。不要先 Flush 再写空行,否则提交出去的可能只是半个事件,客户端仍要等待下一次数据。

可以把“写完整帧并 Flush”封装成一个小函数,让业务循环只关心事件内容。每次 Flush 返回错误时,应把它视为连接已经不可继续使用的信号,退出当前 Handler;不要在同一个连接上无限重试,因为失败重试本身不会重新建立 SSE 连接。

心跳让空闲连接仍有字节流

业务事件可能几分钟才产生一次,但负载均衡器、反向代理或客户端网络设备可能对长时间无数据的连接设置空闲阈值。心跳不需要伪装成业务事件,SSE 的注释行以冒号开头,例如 : ping\n\n,客户端通常会忽略它,但连接链路能看到新的字节。

心跳间隔应小于链路中最短的空闲超时时间,并给网络抖动留出余量。它不是越短越好:连接数量很大时,过密心跳会放大写入、Flush 和代理转发压力。关键是让这个 ticker 与 Handler 的取消信号共享同一个 select,连接结束后立刻停止。

func writeHeartbeat(w http.ResponseWriter) error {
    // SSE 注释不会形成业务事件,但能让空闲连接继续产生字节流。
    _, err := fmt.Fprint(w, ": ping\n\n")
    if err != nil {
        return err
    }
    // 心跳也要 Flush,否则它可能只停留在服务端缓冲中。
    return http.NewResponseController(w).Flush()
}

用 Request.Context 处理客户端断开

对传入的 HTTP 请求,r.Context() 会在客户端连接关闭、HTTP/2 请求被取消,或者 Handler 返回时进入取消状态。SSE 循环应把它当作最高优先级的结束信号:收到取消后停止事件生产,停止 ticker,并从 Handler 返回。

下面的示例假设 events 由上游业务提供。生产者发送事件时也要选择 ctx.Done(),否则 Handler 虽然退出了,上游 goroutine 仍可能阻塞在一个无人接收的 channel 上。

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

func streamHandler(w http.ResponseWriter, r *http.Request, events 

上例的关键不在 ticker 的具体秒数,而在三个清晰的出口:Context 取消、事件源结束、写入或 Flush 失败。若业务事件来自数据库订阅、消息队列或其他 goroutine,应把同一个 ctx 继续传给上游,让上游在客户端离开后也停止等待。

Go SSE ticker 心跳、Context Done、客户端断开和 Handler 退出的静态关系图
图2:心跳负责维持空闲连接的字节流,Request.Context().Done 负责把客户端断开信号传给事件循环。

Flush 不是“客户端已经看到”的证明

Go 官方 Flusher 文档明确提示,即使 ResponseWriter 支持 Flush,如果客户端经过 HTTP 代理,缓冲数据也可能在响应完成前才到达客户端。因此排查“服务端 Flush 了但浏览器没有逐条显示”时,不能只盯着 Handler,还要检查代理是否缓存响应、是否压缩了事件流,以及客户端是否按 SSE 规则解析双换行。

另一个边界是 ResponseWriter 的包装。日志中间件、压缩中间件或自定义响应包装器可能没有实现 Flush,或者没有提供正确的 Unwrap。此时 ResponseController.Flush 会返回不支持错误;如果 SSE 必须经过该中间件,应先确认它是否保留流式写入能力,不要用静默忽略错误来掩盖问题。

把 SSE 的三个生命周期边界放在一起

边界负责什么常见误区
事件格式data: 内容\n\n 组成完整帧只写一行而没有结束空行
发送时机每个事件和心跳写完后调用 Flush认为 Write 返回就代表客户端已看到
空闲保持ticker 发送注释心跳心跳过密或连接断开后 ticker 仍运行
连接结束监听 r.Context().Done() 并返回后台生产者阻塞在无人接收的 channel 上
中间层检查代理缓冲和 ResponseWriter 包装器把 Flush 当成端到端实时性的保证

如果业务还需要给客户端发送 retry、id 或命名事件,可以沿用同一个“写完整帧后 Flush”的边界;不要让不同类型事件走不同的退出和错误处理方式。客户端断开后,服务端通常只需要记录一次断开原因并释放上游订阅,不需要向已经关闭的连接写最后一条消息。

常见追问

直接断言 http.Flusher 还可以吗?

可以用于非常简单的 Handler,但它只能调用无返回值的 Flush(),也不方便处理包装器和底层不支持的情况。新代码可以优先使用 ResponseController.Flush(),把错误纳入连接退出逻辑。

心跳一定要发送 data 事件吗?

不需要。冒号开头的 SSE 注释行适合做连接心跳,客户端不会把它当作业务事件;如果客户端协议明确要求 ping 事件,也可以发送命名事件,但要保持帧格式完整。

为什么 Flush 后仍然看不到实时消息?

可能是代理或压缩层继续缓冲,也可能是事件没有以空行结束,或者客户端没有按 SSE 解析。Flush 只控制 Go HTTP 响应侧的提交动作,不是所有链路的实时显示承诺。

客户端刷新页面后,服务端如何停止旧连接?

在 Handler 内监听 r.Context().Done()。客户端旧连接被关闭后,该 Context 会取消,事件循环和上游生产者都应沿着这个信号返回或停止。

官方资料(复制地址查看):https://pkg.go.dev/net/http#ResponseController、https://pkg.go.dev/net/http#Flusher、https://pkg.go.dev/net/http#Request.Context、https://go.dev/doc/go1.20。本文示例用于说明标准库接口边界,不声称已经在本机运行或代表完整生产配置。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go multipart 上传文件的大小限制实现Go multipart 上传文件的大小限制实现
上一篇
Go multipart 上传文件的大小限制实现
Go ReverseProxy 转发请求头的改写边界
下一篇
Go ReverseProxy 转发请求头的改写边界
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    478次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    535次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    555次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    506次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    328次使用