选WebSocket而非SSE,因需前端主动发控制指令;gorilla/websocket须带token/session鉴权,禁耗时握手;流式转发AI响应需边读边推,设心跳与读写超时,防goroutine泄漏。

选 WebSocket 而不是 SSE,核心就一条:你得让前端能随时发控制指令(比如中断生成、切换模型、重试),而不仅仅是等服务器“单向吐字”。SSE 做不到这点。
gorilla/websocket 连接建立与鉴权
连接不是裸连,必须带身份和上下文。前端 new WebSocket 时 URL 后要拼参,比如 ws://api.example.com/chat?token=xxx&session_id=yyy;后端用 r.URL.Query().Get("token") 提取,再校验 JWT 或查 Redis 会话缓存。别在握手阶段做耗时操作(如查 DB),否则会拖慢连接建立,导致前端 onerror 频发。gorilla 的 Upgrader.CheckOrigin 必须显式设为 func(r *http.Request) bool { return true }(开发期)或按域名白名单校验,否则跨域直接失败。
消息收发格式与 token 流式转发
AI 流式 API(如 OpenAI 的 /v1/chat/completions?stream=true)返回的是 chunked JSON,每帧含 delta.content 字段。你的 Go 服务不能等它全响应完再推——得边读边转。关键点:
AI 舌诊面诊问诊医疗系统 + 诊疗商城系统源码是一套结合 AI 辅助诊断与商品转化的健康类程序,基于浩创云、ThinkPHP(PHP)开发,支持微信服务号与 H5 端,适合做中医舌诊面诊问答、健康建议推送以及配套诊疗商品的运营。
- 用
http.Client.Do发起请求后,从resp.Body拿到io.ReadCloser,用bufio.Scanner按\n\n切分 event-stream - 每扫到一个完整 chunk,
json.Unmarshal解出delta.content,再封装成你自己的协议结构体(比如{Type: "token", Data: "hello"}) - 用
conn.WriteJSON推给前端,别用conn.WriteMessage手动序列化,容易漏转义 - 若 AI 接口返回 error 或 EOF,需主动发
{Type: "error", Message: "..."}给前端,再关闭 conn
连接保活与异常清理
WebSocket 连接空闲时会被中间代理(Nginx、ALB)断开,默认超时往往只有 60 秒。你得自己加心跳:
- 服务端用
time.Ticker每 30 秒调一次conn.WriteMessage(websocket.PingMessage, nil) - 前端收到
ping自动回pong,gorilla 默认处理,不用写逻辑 - 但必须设
Upgrader.KeepAlive = 30 * time.Second,否则底层 TCP keepalive 不生效 - 更关键的是:在
conn.SetReadDeadline和SetWriteDeadline上设超时(比如 65 秒),并在readLoop中捕获net.ErrClosed和websocket.CloseAbnormal,及时从连接池中移除 goroutine
并发模型与资源泄漏风险
每个连接对应一个 goroutine,这是 gorilla 的默认模式,也是最简方案。但必须防住两类泄漏:
- 用户关页后没发
close帧,连接挂着不动——靠上面的ReadDeadline触发超时退出 - AI 接口卡死(如大模型 OOM),goroutine 卡在
scanner.Scan()——必须对 AI 请求设context.WithTimeout,超时后 cancel 并 close body - 别用全局 map 存 conn,改用
sync.Map或带 TTL 的 Redis 缓存 session ID → conn 关系,方便后台主动踢人
真正难的不是连上,是连上之后怎么不崩、不卡、不漏——所有超时、错误、边界状态,都得有明确出口,而不是靠 goroutine 自己结束。

















