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>

四、生产环境的四个注意

  1. Nginx 默认会缓冲 SSE。表现是前端「卡半天然后一次性收到全部内容」。解法是响应头加 X-Accel-Buffering: no(代码里已加),或在 Nginx 配置 proxy_buffering off;。这是上线后最高频的坑。
  2. 客户端断开必须停掉模型调用。r.Context() 在浏览器关闭时取消,NewStreaming 的第一个参数传它即可——否则用户关了页面,模型还在烧钱生成。
  3. 注意 SSE 帧不能含裸换行。文本内容里自带 \n 会破坏帧格式,所以示例用 strconv.Quote 转成 JSON 字符串(单行长文本),前端 JSON.parse 还原。这也是各 AI 产品普遍选择 JSON 编码 chunk 的原因。
  4. 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 + AI 应用开发实战:AI 面试官完整项目、MCP、Agent 工程化,完整源码随文章首发。

微信扫码,或搜索「GO语言入门之路」关注,第一时间获取系列更新与源码,不再错过任何一篇。

wx

关注公众号

©2017-2026 鲁ICP备17023316号-1 Powered by Hugo · 隐私政策