go中sse必须设置四个响应头:content-type: text/event-stream、cache-control: no-cache、connection: keep-alive、access-control-allow-origin: *;每次写data:后须调用flusher.flush(),且需转义换行符、跳过空chunk、末尾发event: end。

Go中sseHandler必须设置的四个响应头
不设对,前端EventSource会直接失败或静默断连。最常漏掉的是Connection: keep-alive——没有它,HTTP/1.1连接会被默认关闭,SSE长连接就断了。
正确设置如下:
-
Content-Type: text/event-stream(强制,否则浏览器不识别为SSE流) -
Cache-Control: no-cache(禁用中间代理缓存,避免消息滞留) -
Connection: keep-alive(维持TCP连接,SSE存活基础) -
Access-Control-Allow-Origin: *(开发阶段必需;生产环境应精确指定域名)
注意:Flusher调用前必须先写响应头并Flush()一次,否则浏览器卡在等待header状态,后续data:全丢弃。
用http.Flusher推送AI分块时的典型陷阱
Go标准库的http.ResponseWriter不保证立即发送数据,必须显式转换为http.Flusher并调用Flush()。但很多人只做一次Flush,导致只有首条消息可见。
关键点:
- 每次写完一个
data: ...块后,**必须紧跟flusher.Flush()** - 不要用
fmt.Println或log.Printf混在流中——它们会往stderr打日志,不进HTTP body - 如果AI模型返回的是JSON chunk(如
{"delta":{"content":"a"}}}),需手动转义换行符:用strings.ReplaceAll(raw, " ", "\n")再拼入data:行,否则SSE解析器会提前截断 - 别在循环里用
time.Sleep模拟延迟——真实AI流是异步的,应从io.ReadCloser边读边推
对接Ollama或OpenAI兼容API时的流式解析逻辑
Ollama的/api/chat和OpenAI的/v1/chat/completions?stream=true都返回逐chunk的JSON Lines(每行一个JSON对象)。Go后端不能等全部响应结束再转发,必须边读边解码、边转SSE格式。
Go语言(Golang)1.26.0版本提供 Go 官方 Windows amd64 MSI 安装包下载入口,版本号 1.26.0,可用于旧项目维护、兼容性测试和指定版本开发环境配置。
实操要点:
- 用
bufio.Scanner按行扫描响应体,scanner.Split(bufio.ScanLines)确保不粘包 - 每行JSON用
json.Unmarshal解析,提取message.content或choices[0].delta.content - 空content(如
delta:{}或finish_reason:"stop")要跳过,否则前端收到空data:会触发错误事件 - 最终流末尾建议发
event: end data: done,让前端明确知道流终结,避免EventSource无限等待
前端用fetch + ReadableStream替代EventSource的原因
虽然EventSource原生支持SSE,但它在AI对话场景下有硬伤:无法带Authorization头、无法捕获4xx/5xx状态码、IE完全不支持。2026年主流项目已转向fetch + ReadableStream。
关键代码片段(前端):
const response = await fetch("/api/chat", {
method: "POST",
headers: { "Authorization": "Bearer " + token, "Content-Type": "application/json" },
body: JSON.stringify({ messages: [...] })
});
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
// 按
分割,提取每个data:块
text.split("
").forEach(chunk => {
const match = chunk.match(/^data:s*(.*)$/m);
if (match && match[1]) appendToChat(match[1]);
});
}
注意:response.body只能读一次,别在response.json()之后再调getReader()——会报body used already错误。
真正难的不是写通SSE,而是处理AI流里那些不规范的chunk:空行、重复data字段、未闭合的JSON、突然中断的连接。这些在本地测试时几乎不出现,一上生产环境就高频触发。建议在flusher.Flush()前后加panic recover,并记录原始字节流用于事后比对。










