SSE 流式输出教程:用 Go 实现大模型打字机效果(Server-Sent Events 原理 + openai-go 实战)
引言:为什么大模型一定要流式输出
系列第 1 篇里我们等模型把整段话写完才返回。体验上是「转圈 20 秒,啪,一大段文字糊你脸上」。大模型生成是逐 token 计算的,后台已经算出前 50 个字时,用户却在干等——流式输出(Streaming)就是让每个 token 算出来就立刻推给前端,实现 ChatGPT 那种打字机效果。首字延迟(TTFT)从秒级降到百毫秒级,是 AI 产品体验的分水岭。
本文分两层:SSE 协议原理(服务端 → 浏览器)+ openai-go 流式 API(模型 → 服务端)。
一、SSE 是什么:被低估的原生方案
SSE(Server-Sent Events)是 HTML5 原生标准,本质是一个不关闭的 HTTP 响应:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
data: {"chunk":"Go 是"}
data: {"chunk":"一门"}
data: {"chunk":"编译型语言"}
协议规则只有三条:响应头声明 text/event-stream;每个消息以 data: 开头、空行结尾;服务端每写一帧就 flush 一次,连接保持不断。浏览器侧用 EventSource 接收,onmessage 每帧回调一次。
和 WebSocket 对比,为什么是 SSE 赢了 AI 场景:
| 维度 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 服务端 → 客户端单向 | 双向 |
| 协议 | 普通 HTTP,自带重连 | 独立协议,需自己管心跳重连 |
| 实现成本 | 标准库即可 | 需第三方库 |
| 穿透性 | 走 80/443,代理友好 | 部分代理/网关需特殊配置 |
| 适用场景 | 流式输出、通知推送 | 聊天室、协同编辑、游戏 |
大模型输出恰恰是严格单向的:浏览器把问题 POST 上去,之后只需要一个字一个字地收。SSE 就是为此量身定做的。
二、模型侧:openai-go 流式 API
SDK 提供 NewStreaming,用法和普通的 New 几乎一样,返回一个可迭代的流对象(对照 v3.96.0 官方示例核实):
stream := client.Responses.NewStreaming(ctx, responses.ResponseNewParams{
Model: openai.ChatModelGPT5_2,
Input: responses.ResponseNewParamsInputUnion{
OfString: openai.String("写一首关于编程的俳句"),
},
})
defer func() { _ = stream.Close() }()
for stream.Next() {
event := stream.Current()
print(event.Delta) // 增量文本,逐 token 到达
}
if stream.Err() != nil {
// 处理错误
}
三、实战:端到端的流式聊天服务
把两段拼起来:模型流 → Go 服务端 → 浏览器。服务端用标准库 net/http,不引入任何额外依赖:
package main
import (
"context"
"fmt"
"net/http"
"os"
"strconv"
"time"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/option"
"github.com/openai/openai-go/v3/responses"
)
var client *openai.Client
func send(w http.ResponseWriter, flusher http.Flusher, payload string) {
fmt.Fprintf(w, "data: %s\n\n", strconv.Quote(payload)) // JSON 字符串保证单行
flusher.Flush() // 立即推给浏览器
}
func chatHandler(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("X-Accel-Buffering", "no") // 关键:禁用 Nginx 缓冲,见下文
// 客户端断开连接时,ctx 自动取消,模型调用随之停止(省 token)
stream := client.Responses.NewStreaming(r.Context(), responses.ResponseNewParams{
Model: openai.ChatModelGPT5_2,
Input: responses.ResponseNewParamsInputUnion{
OfString: openai.String(r.URL.Query().Get("q")),
},
})
defer func() { _ = stream.Close() }()
for stream.Next() {
event := stream.Current()
if event.Delta == "" {
continue
}
send(w, flusher, event.Delta)
}
if err := stream.Err(); err != nil && err != context.Canceled {
send(w, flusher, "[error] "+err.Error())
}
send(w, flusher, "[done]") // 结束标记,前端收到后主动关闭连接
}
func main() {
client = openai.NewClient(option.WithAPIKey(os.Getenv("OPENAI_API_KEY")))
http.HandleFunc("/chat", chatHandler)
srv := &http.Server{Addr: ":8080", ReadHeaderTimeout: 10 * time.Second}
_ = srv.ListenAndServe()
}
前端只需几行(假设页面由同端口提供):
<script>
const es = new EventSource('/chat?q=' + encodeURIComponent('用三句话介绍 Go 语言'));
es.onmessage = (e) => {
const chunk = JSON.parse(e.data); // 服务端发的是 JSON 字符串
if (chunk === '[done]') { es.close(); return; }
document.body.innerText += chunk;
};
es.onerror = () => es.close();
</script>
四、生产环境的四个注意
- Nginx 默认会缓冲 SSE。表现是前端「卡半天然后一次性收到全部内容」。解法是响应头加
X-Accel-Buffering: no(代码里已加),或在 Nginx 配置proxy_buffering off;。这是上线后最高频的坑。 - 客户端断开必须停掉模型调用。
r.Context()在浏览器关闭时取消,NewStreaming的第一个参数传它即可——否则用户关了页面,模型还在烧钱生成。 - 注意 SSE 帧不能含裸换行。文本内容里自带
\n会破坏帧格式,所以示例用strconv.Quote转成 JSON 字符串(单行长文本),前端JSON.parse还原。这也是各 AI 产品普遍选择 JSON 编码 chunk 的原因。 - EventSource 只能 GET。复杂参数建议先 POST 一个
/session拿到 ID,再EventSource('/stream?id=xxx');或者前端用fetch+ReadableStream手动读 SSE(可以 POST),本系列为聚焦原理采用 GET + 查询参数的最简形态。
五、速查清单
| 概念 | 一句话解释 |
|---|---|
| SSE | 不关闭的 HTTP 响应,逐帧推送 data: 消息 |
| text/event-stream | SSE 的 Content-Type 声明 |
| http.Flusher | 标准库接口,Flush() 把缓冲立刻发出去 |
| X-Accel-Buffering: no | 告知 Nginx 不要缓冲响应 |
| NewStreaming | openai-go 发起流式请求,Next()/Current()/Err() 遍历 |
| Delta | 流式事件里的增量文本 |
版本说明
- 本文基于 openai-go v3.96.0(2026-11-06 核对,GitHub releases 最新 tag v3.96.0,发布于 2026-11-05);安装命令
go get github.com/openai/openai-go/v3@v3.96.0。 - 该版本要求 Go 1.26 或更高版本(官方 README 声明,历次核对一致)。
- 文中流式 API(
Responses.NewStreaming、stream.Next()、stream.Current()、event.Delta、stream.Err())逐字对照 v3.96.0 官方 README「Streaming responses」小节与examples/responses-streaming/main.go核实;SSE 服务端与http.Flusher均为 Go 标准库接口。 - 官方文档:pkg.go.dev/github.com/openai/openai-go/v3 · html.spec.whatwg.org(EventSource 规范)
本文是「Go AI 应用开发」系列第 4 篇(共 18 篇规划)。上一篇:《Embedding 入门教程:语义检索原理、Go 实现与余弦相似度搜索实战》;系列前两篇:《Function Calling 教程》、《大模型 API 基础》。至此阶段一(LLM 基础)四篇全部完成,下一篇预告:《Go AI 框架选型横评:Eino vs LangChainGo vs Genkit vs GoAI SDK》。
- 《Go 程序设计语言》(Go 语言圣经,机械工业出版社) —— Go 官方团队出品,公认入门第一书
- 《Go 语言实战》(异步图书,人民邮电出版社) —— 实战导向,面试前巩固工程手感
- 《Go 语言编程之旅:一起用 Go 做项目》(博文视点,电子工业出版社) —— 项目驱动入门,从零写出完整应用
- Keychron K1Max 矮轴机械键盘(Mac/Win 双系统,QMK/VIA 开源改键) —— 轻薄矮轴 + 开源固件,程序员桌面常客
- 罗技 MX Master 3S 无线鼠标(Flow 跨屏,静音电磁滚轮) —— 人体工学设计,写代码一整天手不酸
以上链接含京东联盟推广代码;通过链接购买本站可能获得少量佣金,不影响你的购买价格。
每周更新 Go + AI 应用开发实战:AI 面试官完整项目、MCP、Agent 工程化,完整源码随文章首发。
微信扫码,或搜索「GO语言入门之路」关注,第一时间获取系列更新与源码,不再错过任何一篇。