直接用 net/http + gorilla/websocket 因其稳定、轻量、兼容性强,且能无缝集成 http.ServeMux;需通过 Upgrader.Upgrade 获取 *websocket.Conn 才可操作,严禁直接用 ResponseWriter 写帧。

为什么直接用 net/http + gorilla/websocket 而不选其他库
Go 官方 net/http 本身不提供 WebSocket 协议支持,必须依赖第三方实现。目前最稳定、被生产环境广泛验证的是 gorilla/websocket——它不依赖任何外部 HTTP 框架,能无缝嵌入现有 http.ServeMux 或任何兼容 http.Handler 的服务中。比 gobwas/ws 更成熟,比 websocket(nhooyr.io)默认启用更多安全限制(如 origin 检查),更适合快速上线。
常见错误是试图用 http.ResponseWriter 直接写 WebSocket 帧,这会 panic 并报错 http: response.WriteHeader on hijacked connection。WebSocket 必须通过 Upgrader.Upgrade() 获取 *websocket.Conn 实例后操作。
如何正确升级 HTTP 连接并处理并发读写
WebSocket 连接是全双工的,但 *websocket.Conn 的 ReadMessage() 和 WriteMessage() 默认不是并发安全的:多个 goroutine 同时调用会导致 panic 或数据错乱。必须显式分离读/写逻辑。
- 用
upgrader.CheckOrigin = func(r *http.Request) bool { return true }临时绕过跨域检查(上线前务必改回严格校验) - 每个连接启动两个 goroutine:一个阻塞读(
ReadMessage),一个从 channel 接收消息后写(WriteMessage) - 写操作必须加锁或用带缓冲 channel 控制速率,否则
WriteMessage可能阻塞甚至因对端断连 panic - 务必在 defer 中调用
conn.Close(),否则连接泄漏,fd 耗尽后服务拒绝新连接
示例关键片段:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
gorilla/websocket
- 设置
upgrader.KeepAlive = 30 * time.Second让底层 TCP 保活 - 调用
conn.SetPingHandler()指定 pong 回调(默认已注册,无需重写) - 在写协程里定期调用
conn.WriteMessage(websocket.PingMessage, nil),注意必须在写锁保护下或确保不与普通 WriteMessage 冲突 - 读协程需捕获
websocket.CloseMessage类型消息,主动退出;否则ReadMessage返回io.EOF后仍继续循环会反复 panic
粘包问题不存在于 WebSocket 层——每帧自带长度头,ReadMessage 自动按帧拆分。但若业务层把多条 JSON 拼成一个字符串发送,接收端就得自己解析边界,这不是协议问题,是序列化设计失误。
如何管理连接生命周期和广播性能瓶颈
用全局 map 存连接(map[*websocket.Conn]bool)看似简单,但并发读写 map 会 crash。必须加 sync.RWMutex,且不能在遍历 map 时直接调用 conn.WriteMessage——某个连接卡住会拖慢全部广播。
- 用
sync.Map替代普通 map,避免读写锁竞争(但注意sync.Map的 range 不保证原子性) - 广播时先用
sync.Map.Range()收集所有活跃连接到 slice,再遍历写,失败则从 map 中删除该连接 - 高并发场景下,单个 broadcast channel 可能成为瓶颈,可考虑分片(shard)或按 topic 分 channel
- 连接数超 1k 后,频繁的
time.Now()和日志打印会明显拖慢,建议用log.SetFlags(0)关闭时间戳,或异步打点
真正难的从来不是“怎么连上”,而是连接断了怎么感知、消息丢了怎么补偿、百万连接时内存和 GC 怎么扛——这些细节不压测根本看不出问题。










