应使用 gorilla/websocket 而非 net/http 手动升级;需配置 CheckOrigin、读写分离 goroutine、sync.RWMutex 管理连接 map、心跳保活与超时清理。

用 gorilla/websocket 建连接,别碰 net/http 原生升级
Go 标准库没提供开箱即用的 WebSocket 服务端支持,硬用 net/http 手动处理 Upgrade 头、校验 Sec-WebSocket-Key 等细节极易出错。实际项目中几乎没人这么干。
直接上 gorilla/websocket——它封装了握手、ping/pong、消息帧解析、连接生命周期管理,且维护活跃、文档清晰。
- 初始化连接时,用
upgrader.Upgrade()替代手写http.ResponseWriter操作 - 设置
CheckOrigin回调,否则跨域页面连不上(默认只允许同源) - 连接建立后,
*websocket.Conn是线程不安全的,读写需加锁或分 goroutine 处理(比如一个 goroutine 专读,一个专写)
消息广播必须自己管并发,map + sync.RWMutex 是底线
没有“内置广播”这回事。所有在线连接得你自己存、遍历、发。常见错误是直接用裸 map[*websocket.Conn]bool 并发读写,立刻 panic。
至少做到:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 用
sync.RWMutex包裹连接 map,读多写少场景下比sync.Mutex更友好 - 每个连接对应一个 goroutine 处理
ReadMessage(),收到消息后塞进中心 channel,由单独广播 goroutine 统一分发 - 发消息前检查
conn.WriteMessage()返回值,网络断开时会报websocket: close sent或io: read/write closed,得及时从 map 中清理连接
WriteMessage() 阻塞?先确认你没在同一个 conn 上并发读写
很多人一遇到发送卡住,就去调 SetWriteDeadline() 或换缓冲区大小,其实八成是误用了连接对象。
gorilla/websocket 明确要求:同一 *websocket.Conn 上,读和写操作不能并发执行。这不是性能问题,是协议层约束。
- 不要在处理
ReadMessage()的 goroutine 里直接调WriteMessage() - 正确做法是把要发的消息发给广播 goroutine,由它统一调
WriteMessage() - 如果真需要单连接响应(比如回 echo),用
conn.SetWriteDeadline()设个超时,避免无限卡住
客户端断网时,服务端不会立刻知道,靠 SetPingHandler + 心跳保活
TCP 连接断开后,服务端可能几十秒甚至几分钟都收不到 EOF。用户关浏览器、切后台、WiFi 断开,你的 map 里还留着“僵尸连接”,广播时反复失败。
必须主动探测:
- 服务端调
conn.SetPingHandler(),并定期用conn.WriteMessage(websocket.PingMessage, nil)发心跳 - 客户端收到 ping 要回 pong(
gorilla默认自动处理,不用写) - 同时设
conn.SetReadDeadline(),每次成功读消息或 pong 后更新 deadline,超时未活动就关连接
这个逻辑漏掉,聊天室上线两天后在线数虚高、广播变慢、日志刷满 write closed 错误,都是必然结果。

















