用 Go 实现 Server-Sent Events(SSE):让 AI 处理进度“看得见”!
{"status":"processing", "progress":20, "message":"正在理解语义..."}{"status":"processing", "progress":60, "message":"生成摘要中..."}{"status":"done", "result":"这是一段精炼的摘要。"}Message string `json:"message"` // 人类可读
·
✍️ 适合人群:
- 写过 REST API,但用户总问“好了没?”的你
- 想告别“轮询 polling”,拥抱“推送 push”的你
- 正在搞 AI 服务、视频转码、批量导入等长耗时任务的你
🤔 为什么选 SSE?而不是 WebSocket 或轮询?
| 方案 | 特点 | 适用场景 |
|---|---|---|
| 轮询(Polling) | 前端每 2s 问一次“好了没?” → 浪费带宽 + 延迟高 | 简单、低频任务(如每分钟刷新状态) |
| WebSocket | 双向通信,强大但重 | 聊天室、实时协作、游戏 |
| ✅ SSE | 服务端单向推流 + 自动重连 + 文本协议 + 原生浏览器支持 | ✅ 任务进度、通知、日志流、AI 响应流 |
💡 关键优势:
- 前端用
EventSource一句代码接入- 自动重连(网络抖动不怕)
- HTTP 协议,CDN/代理友好
- 服务端就是普通 HTTP handler —— Go 写起来超简单!
🧪 小实战:AI 文本摘要服务(模拟)
🎯 需求:用户提交一段长文本,后端“调用 AI”处理(模拟耗时 5s),期间推送:
{"status":"processing", "progress":20, "message":"正在理解语义..."}{"status":"processing", "progress":60, "message":"生成摘要中..."}{"status":"done", "result":"这是一段精炼的摘要。"}
✅ 1. 定义事件消息结构
// event.go
type SSEMessage struct {
Status string `json:"status"` // "processing" / "done" / "error"
Progress int `json:"progress"` // 0-100
Message string `json:"message"` // 人类可读提示
Result string `json:"result,omitempty"` // 最终结果(仅 done 时有)
}
📝 用 JSON 统一格式,方便前端统一处理 👍
✅ 2. SSE Handler 核心写法 —— 关键 4 步
// sse_handler.go
func AIStreamHandler(w http.ResponseWriter, r *http.Request) {
// Step 1️⃣: 设置响应头 → 告诉浏览器:这是 SSE!
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*") // 可选:允许跨域
// Step 2️⃣: 强制 flush,让 header 立刻发出去(否则浏览器等 body)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
// Step 3️⃣: 模拟 AI 处理流程(用 goroutine + channel 控制节奏)
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
// 用一个 channel 模拟“AI 步骤事件流”
events := make(chan SSEMessage, 10)
go simulateAIWork(events)
// Step 4️⃣: 主循环:监听事件 → 推送给前端
for {
select {
case msg := <-events:
data, _ := json.Marshal(msg)
// 格式:`data: {...}\n\n`
fmt.Fprintf(w, "data: %s\n\n", data)
if f, ok := w.(http.Flusher); ok {
f.Flush() // ⚠️ 每次写完必须 flush!否则缓冲区卡住
}
// 任务结束?跳出循环(否则 keep-alive 会一直挂)
if msg.Status == "done" || msg.Status == "error" {
return
}
case <-ctx.Done():
// 超时或客户端断开
fmt.Fprintf(w, "data: %s\n\n", `{"status":"error","message":"Client disconnected or timeout"}`)
return
}
}
}
🔑 关键细节提醒:
- ✅
text/event-stream是 SSE 的“身份证”- ✅
Flush()是灵魂!不 flush 就像快递打包了不发货 📦- ✅ 每条消息以
\n\n结尾,这是 SSE 协议要求- ✅ 用
context监听客户端断开(浏览器关 tab 时自动 cancel)
✅ 3. 模拟 AI 工作流(带进度)
// simulate.go
func simulateAIWork(out chan<- SSEMessage) {
defer close(out)
send := func(msg SSEMessage) {
select {
case out <- msg: // 防 channel 关闭 panic
default:
}
}
// Step 1: 预处理
send(SSEMessage{Status: "processing", Progress: 10, Message: "接收请求..."})
time.Sleep(500 * time.Millisecond)
// Step 2: 理解语义
send(SSEMessage{Status: "processing", Progress: 30, Message: "正在理解语义结构..."})
time.Sleep(1200 * time.Millisecond)
// Step 3: 生成摘要
send(SSEMessage{Status: "processing", Progress: 70, Message: "生成摘要草稿..."})
time.Sleep(1500 * time.Millisecond)
// Step 4: 优化润色
send(SSEMessage{Status: "processing", Progress: 95, Message: "润色语言,提升可读性..."})
time.Sleep(800 * time.Millisecond)
// Step 5: 完成!
send(SSEMessage{
Status: "done",
Progress: 100,
Message: "✅ 摘要生成成功!",
Result: "Go 的 SSE 实现简洁高效,特别适合单向实时推送场景,如 AI 处理进度、日志流、通知等。",
})
}
✅ 4. 前端代码:3 行接入 SSE!
<!-- index.html -->
<script>
const evtSource = new EventSource("/ai/summarize");
evtSource.onmessage = (event) => {
const msg = JSON.parse(event.data);
console.log("🚀 收到进度:", msg);
// 比如更新页面
document.getElementById("status").innerText = msg.message;
document.getElementById("progress").style.width = msg.progress + "%";
if (msg.status === "done") {
document.getElementById("result").innerText = msg.result;
evtSource.close(); // 任务完成,关连接
}
};
evtSource.onerror = (err) => {
console.error("❌ SSE Error", err);
evtSource.close();
};
</script>
<div id="status">等待 AI 响应...</div>
<div style="width:100%; background:#eee; height:20px; margin:10px 0;">
<div id="progress" style="height:100%; background:#4CAF50; width:0%"></div>
</div>
<pre id="result"></pre>
✅ 效果:进度条实时前进 + 最终显示摘要 ✅
🛠 路由 & 启动(完整可运行)
// main.go
package main
import (
"encoding/json"
"fmt"
"net/http"
"time"
)
func main() {
http.HandleFunc("/ai/summarize", AIStreamHandler)
fmt.Println("🚀 Server running on http://localhost:8080")
http.ListenAndServe(":8080", nil)
}
✅ 运行:
go run .
# 访问 http://localhost:8080/index.html
🧩 进阶技巧(来自生产经验)
| 问题 | 解决方案 |
|---|---|
| 客户端断开后,Go 还在发? | 用 r.Context().Done() 监听,或 w.(http.CloseNotifier)(旧版)→ 推荐 context |
| 想区分事件类型? | 用 event: xxx 字段:fmt.Fprintf(w, "event: progress\ndata: %s\n\n", data)前端: evtSource.addEventListener("progress", ...) |
| 连接数太多? | 加 middleware 限流:gorilla/handlers or 自定义计数器 |
| 想支持重连 token? | 客户端传 ?lastEventId=123,服务端从断点恢复 |
| 调试看 raw stream? | curl -N http://localhost:8080/ai/summarize(-N 关闭缓冲) |
💡 小知识:SSE 默认 3 秒无数据会触发浏览器重连(可配
retry: 5000控制)
🌌 哲思:SSE 是“耐心”的技术
今天的世界崇尚“即时满足”——
但有些事,值得等待:一杯手冲咖啡、一段深刻思考、一次 AI 的深度推理。SSE 不是炫技,而是对用户的尊重:
“我知道你在等,所以我告诉你:我正在努力。”这比一句冷冰冰的
202 Accepted,温暖得多。
更多推荐



所有评论(0)