Go HTTP流式响应必须显式声明Accept: text/event-stream,否则即使后端支持SSE也返回普通JSON;需用bufio.Scanner按行解析data:块,跳过空行和注释,剥离前缀后json.Unmarshal,禁用io.ReadAll和json.Decode以防卡死或panic。

Go HTTP流式响应必须显式声明Accept: text/event-stream
不加这句头,哪怕后端支持SSE,http.Client 也会收到普通JSON响应,而不是逐块的 data: {...}。常见错误是只设 stream: true 却漏掉 Accept 头,结果调试半天发现响应体根本不是流格式。
- 用
req.Header.Set("Accept", "text/event-stream"),别写成"application/json"或留空 - OpenAI、Ollama、AnythingLLM 等兼容服务都严格校验这个 header
- 某些反向代理(如 Nginx)会默认 strip 掉非标准 Accept 头,需显式配置
proxy_pass_request_headers on;
用bufio.Scanner解析SSE,别碰io.ReadAll或json.Decode
io.ReadAll(resp.Body) 会卡死到连接关闭,彻底失去流式意义;json.NewDecoder(resp.Body).Decode() 则直接 panic:SSE 不是合法 JSON,开头就是 data: {,报错 invalid character 'd' looking for beginning of value。
- 用
scanner := bufio.NewScanner(resp.Body)+ 自定义SplitFunc拆分data:块最稳 - 必须跳过空行和注释行(
: ping),否则 scanner 位置错乱,后续scanner.Text()取到的是上一块的残留 - 每块开头检查是否以
"data: "开始,再用json.Unmarshal([]byte(line[6:]), &chunk)解析内容
gRPC双向流要分开读写协程,CloseSend不能忘
客户端若只在一个 goroutine 里交替 Send 和 Recv,极易因阻塞导致死锁——比如服务端还没发回响应,客户端就卡在 Send 上,再也收不到任何数据。
- 发送逻辑放单独 goroutine,最后务必调用
stream.CloseSend()通知服务端“我不发了” - 接收逻辑在主 goroutine 或另一 goroutine 中循环
Recv(),遇到io.EOF表示服务端已关闭写入 - 服务端不用
CloseSend,但每次Send后得检查 err,网络断开时Send会立即失败
流处理中goroutine退出时机最容易被忽略
main 函数一 return,所有 goroutine 强制终止,正在处理的流数据直接丢弃——这不是 bug,是 Go 的设计事实。没人等你,除非你主动管。
立即学习“go语言免费学习笔记(深入)”;
- 用
sync.WaitGroup记录启动的流处理协程数,每个协程结束前wg.Done() - 别在 for-range channel 里直接启 goroutine 而不传参,循环变量会被覆盖,10 个协程可能全处理最后一个 item
- 超时控制要设在
http.Client.Timeout和context.WithTimeout两层,光设 client timeout 不拦得住已建立但卡住的流连接


















