gorilla/websocket 是唯一靠谱选择:专注协议层,upgrader 处理握手,*websocket.conn 提供安全读写,明确并发限制,需独立读/写 goroutine、房间隔离、writech 通道、deadline 重置及心跳处理。

gorilla/websocket 是唯一靠谱选择
别折腾标准库 net/http 手动处理 Upgrade 和帧解析,也别用封装过重的框架(比如带 ORM 或模板引擎的 Web 框架)套 WebSocket。直接上 gorilla/websocket:它专注协议层,不侵入业务逻辑,Upgrader 处理握手,*websocket.Conn 提供 ReadMessage/WriteMessage,且明确文档写清了并发限制——这是你后续做安全广播的前提。
常见错误是拿 echo 或 gin 的中间件链去“包装” WebSocket 路由,结果心跳被中间件阻塞、连接状态被复用、甚至 conn 被提前关闭。正确做法是:HTTP 路由只负责升级,之后完全脱离框架生命周期。
-
Upgrader.CheckOrigin生产环境必须校验 Origin,不能设为func(r *http.Request) bool { return true } - 连接建立后立刻调用
conn.SetPingHandler和conn.SetPongHandler,否则客户端发 ping 后服务端不响应 pong,连接会被静默断开 - 每个
*websocket.Conn必须绑定独立的读/写 goroutine,不能在 handler 函数里串行读写
sync.Map + 每连接 writeCh 是并发写安全的底线
*websocket.Conn.WriteMessage 不是并发安全的——两个 goroutine 同时调用会 panic。而广播必然触发多路写,所以必须隔离写操作。最简方案:每个 client 结构体带一个 writeCh chan []byte,启动专属写协程从该 channel 读并调 WriteMessage。
别用 sync.Mutex 包一层然后全局锁写,那会把广播变成串行,100 个用户就卡住;也别用无缓冲 channel 直接 send,一旦某个 client 网络卡住,整个广播就堵死。
- 写协程必须用
select+default或带超时的send,避免因单个 client 阻塞拖垮全链路 - 连接关闭时务必
close(client.writeCh),否则写协程会在上永久阻塞 -
sync.Map用于存 client,但注意:不要用LoadOrStore注册连接——若 key 已存在,它不会覆盖 value,导致旧连接残留
房间隔离必须按 room ID 拆分连接池
所有连接塞进一个全局 sync.Map 然后遍历广播,等于把 A 房间消息发给 B 房间用户。真正的房间隔离不是靠消息里带 "room": "xxx" 字段过滤,而是连接建立时就归属到对应房间实例。
URL 带参数 ?room=general,服务端用 r.URL.Query().Get("room") 提取,再查或创建 Room{ id: "general", clients: sync.Map{} }。每个房间维护自己的 clients 和独立的广播 channel。
- 客户端发送的
room、username字段一律丢弃,只信任服务端注入的字段(如登录后生成的userID) - 空闲房间要定时检查:若
clients.Len() == 0且超过 5 分钟没新连接,就从全局 rooms map 中 delete 掉,防止内存泄漏 - 广播时跳过发送方:比对当前消息来源 conn 和遍历中的 conn 指针是否相等,而不是比对 userID —— 同一用户可能多端登录
写操作必须设 deadline,且每次写前重置
conn.SetWriteDeadline 不是“设一次管全程”,而是每次 WriteMessage 前都得调。否则网络抖动时写卡住,deadline 过期后下一次写直接失败,且错误不抛出,消息就静默丢失。
典型场景:用户手机切到后台,TCP 连接未断但写缓冲区满,WriteMessage 阻塞数秒。若没 deadline,整个写协程卡死;若有但没重置,第二次写沿用过期时间,立刻返回 net.ErrTimeout。
- 推荐封装
writeJSONSafe函数:先conn.SetWriteDeadline(time.Now().Add(10 * time.Second)),再defer conn.SetWriteDeadline(time.Time{})清掉 - 写失败时不要重试,直接
close(writeCh)并从房间 clients 中删掉该 conn - 心跳消息也要走同一 writeCh,否则可能因写队列积压导致 pong 响应延迟,触发客户端主动断连
真正难的不是连上 WebSocket,而是让 100 个连接同时稳定收发不 panic、不丢消息、不泄漏 goroutine。每个 conn 的写协程、每个 room 的独立连接池、每次写前的 deadline 重置——这三件事漏掉任何一环,系统在压测时就会开始掉连接。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











