必须分开处理协议升级:SSE需设置Content-Type、Cache-Control、Connection头并启用Flusher;WebSocket需用Upgrader.Upgrade()完成HTTP升级,二者不可混用同一handler或共享连接存储。

用 Mux 注册 SSE 和 WebSocket 路由时,必须分开处理协议升级
Go 的 net/http 本身不区分协议语义,gorilla/mux 也只是路径匹配器——它无法自动识别并分流 text/event-stream 或 Upgrade: websocket 请求。你不能指望一个 HandleFunc 同时处理 SSE 流式写入和 WebSocket 的双向消息收发。错误做法是:把两个逻辑塞进同一个 handler 函数里,靠 r.Header.Get("Accept") 或 r.Header.Get("Upgrade") 分支判断,这会导致:WriteHeader 调用冲突、连接状态混乱、flush() 失效、甚至 panic。
正确方式是为两类协议注册独立路由,并在 handler 内部做协议专属初始化:
- SSE 路由必须显式设置
Content-Type: text/event-stream、Cache-Control: no-cache、Connection: keep-alive,且 handler 内要启用http.Flusher接口并调用Flush() - WebSocket 路由必须用
websocket.Upgrader.Upgrade()显式完成 HTTP 升级,之后所有读写都走*websocket.Conn,不能再碰http.ResponseWriter - 两者共用同一路径(如
/stream)是可行的,但需确保 upgrade 检查顺序:先判Upgrade == "websocket",再判Accept == "text/event-stream",否则 WebSocket 请求可能被误当成 SSE
避免在 Mux 中对同一路径混用 Handler 和 HandleFunc
常见错误是这样写:
r.HandleFunc("/stream", sseHandler).Methods("GET")
r.HandleFunc("/stream", wsHandler).Methods("GET")
这不会报错,但后者会完全覆盖前者——gorilla/mux 按注册顺序匹配,只取最后一个。更隐蔽的问题是:你可能用 Handle() 注册了一个自定义 http.Handler 实例,又用 HandleFunc() 注册函数,结果因接口实现细节导致 ServeHTTP 被跳过或执行两次。
推荐统一用结构体实现 http.Handler,并在 ServeHTTP 中做协议分发:
- 检查
r.Header.Get("Upgrade") == "websocket"→ 走upgrader.Upgrade() - 检查
r.Header.Get("Accept") == "text/event-stream"→ 设置头 + 启动流式写入 - 其余情况返回
http.StatusNotAcceptable - 注意:所有分支都必须显式调用
w.WriteHeader()或upgrader.Upgrade(),否则连接挂起
WebSocket 连接管理与 SSE 客户端生命周期不能共享 map
有人试图用同一个 map[string]http.ResponseWriter 同时存 SSE 连接和 WebSocket 连接,这是危险的。SSE 的 http.ResponseWriter 是一次性的、不可重入的;而 WebSocket 的 *websocket.Conn 是长生命周期对象,其底层 TCP 连接由 Go 自行维护。混存会导致:
开箱即用的技能链路由引擎。13 条预定义链覆盖搜索、开发、审查、MLOps、法律、创意等场景,三层路由架构(触发词→SAD反馈→DAG编排),recall@10=96.97%。配置驱动(chains.yaml),零代码扩展。pip install skill-weave-chains 一键安装。
- 向已关闭的 SSE response 写数据触发
write tcp: use of closed network connection - 对
*websocket.Conn调用Flush()报错(该方法不存在) - 并发读写 map 时 panic,除非你为每个协议配独立锁(
sync.RWMutex),但没必要
生产环境应严格分离:
- SSE 客户端用
map[http.ResponseWriter]struct{}+ 全局sync.RWMutex,每次写前检查连接是否仍存活(可加心跳字段) - WebSocket 连接用
map[string]*websocket.Conn+ 独立sync.RWMutex,键名用业务 ID(如user_id),不是RemoteAddr - 两者清理时机不同:SSE 连接断开后 handler 自然退出;WebSocket 必须在
ReadMessage()返回 error 后显式从 map 删除
子路由与中间件顺序影响协议兼容性
如果你用 r.PathPrefix("/api").Subrouter() 套一层子路由,再在子路由里注册 SSE/WS,要注意中间件的执行时机。例如日志中间件若在 Upgrade 之前调用了 io.Copy(ioutil.Discard, r.Body),就会消费掉 WebSocket 握手 body,导致 upgrader.Upgrade() 失败并返回 websocket: the client is not using the websocket protocol。
关键原则:
- 所有涉及
r.Body读取、w.Header().Set()修改、或依赖完整请求体的中间件,必须放在协议判定之后(即放到子路由 handler 内部,而非外层中间件链) - 鉴权类中间件可以前置,但不能阻断 upgrade 请求——JWT 校验通过后,必须原样透传请求给下游 handler
- 若用
r.Use(authMiddleware, loggingMiddleware),loggingMiddleware 里要跳过Upgrade请求,或改用条件日志(如仅记录非Upgrade请求)
最稳妥的方式是:协议分发逻辑作为顶层 handler,鉴权和基础 header 设置放在这里;具体业务逻辑(如广播、权限校验)下沉到子 handler,避免中间件污染协议握手阶段。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










