应使用 gorilla/websocket 而非 net/http 裸升级,因其封装完整帧处理、心跳、错误处理;需正确配置 Upgrader、子协议、读写超时与并发模型,并安全广播及清理连接资源。

用 gorilla/websocket 建连接,别碰标准库 net/http 的裸升级
Go 标准库没有原生 WebSocket 实现,net/http 里所谓“升级”只是帮你处理了握手头,后续帧读写、掩码校验、心跳、关闭流程全得自己写。99% 的场景直接用 gorilla/websocket —— 它是事实标准,稳定、有完整错误处理、支持 ping/pong 自动响应。
常见错误现象:websocket: bad handshake(没走 Upgrader.Upgrade())、连接后立刻断开(没调 conn.SetPingHandler() 或忽略 io.EOF)、并发读写 panic(没加锁或没用 conn.SetReadDeadline())。
-
Upgrader.CheckOrigin默认拒绝非同源请求,开发时设为func(r *http.Request) bool { return true },上线必须收紧 -
Upgrader.Subprotocols要和前端new WebSocket(url, ['json'])的子协议严格匹配,否则握手失败 - 每个连接建议起独立 goroutine 处理读,再用 channel 向写协程投递消息,避免读写竞争
conn.ReadMessage() 和 conn.WriteMessage() 别混用阻塞模式
这两个函数默认是同步阻塞的,但 WebSocket 连接天然双向,读卡住时写也会被拖住。尤其在广播场景下,一个慢客户端会拖垮整个写逻辑。
使用场景:实时日志推送、聊天室消息分发、行情更新 —— 都要求写不被单个连接阻塞。
立即学习“go语言免费学习笔记(深入)”;
- 对写操作加超时:
conn.SetWriteDeadline(time.Now().Add(10 * time.Second)),写失败就关连接 - 不要在
ReadMessage()循环里直接调WriteMessage(),改用带缓冲的 channel 中转 - 二进制消息优先用
conn.WriteMessage(websocket.BinaryMessage, data),文本消息注意 UTF-8 合法性,非法字节会触发websocket: bad message
如何安全地广播消息给所有在线连接
没有全局连接列表,靠 map + mutex 管理容易漏锁、panic 或遍历时连接已关闭。更糟的是,直接遍历并调 WriteMessage() 会因某个连接卡住而阻塞整个广播。
性能影响:1000 个连接,每次广播都同步写,延迟毛刺明显;用 channel + worker 模式可压到毫秒级抖动。
- 用
sync.Map存*websocket.Conn,key 用自增 ID 或 session token,避免 map 并发读写 panic - 每个连接启动时注册到全局 map,defer 里反注册;注册/反注册操作必须原子
- 广播时只往每个连接专属的
chan []byte发消息,由独立的 write goroutine 消费,失败则关连接并清理 map
连接异常中断时,io.EOF 和 websocket.CloseMessage 怎么区分处理
客户端正常关闭会发 CloseMessage 帧,服务端收到后应主动调 conn.Close();而网络闪断、浏览器标签关闭、手机切后台,服务端往往先读到 io.EOF 或 websocket: close sent 错误。
容易踩的坑:把 io.EOF 当普通错误 log 并重试,结果反复尝试向已断开连接写数据,触发 write: broken pipe;或忽略 CloseMessage 导致连接资源泄漏。
- 读循环中,
err == io.EOF或strings.Contains(err.Error(), "use of closed network connection")可安全退出 - 收到
websocket.CloseMessage类型帧,必须立即调conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))再关连接 - 所有连接关闭前,务必从全局 map 清除,否则内存泄漏随时间线性增长
最常被忽略的是连接关闭后的资源清理时机 —— 不是 defer 里关 conn 就完事,map 里的引用、channel 的接收端、定时器都要一并释放。










