应选github.com/gofiber/contrib/websocket,因其封装gorilla/websocket并集成fiber生命周期,自动处理连接升级、跨域校验、上下文透传;裸用gorilla需手动实现中间件、header解析及用户信息传递,易导致ctx.locals为空、跨域拦截或鉴权失效。

用 Go Fiber 搭建 WebSocket 聊天室,关键不在“能不能跑”,而在「连接不丢、消息不漏、并发不崩」——这三点没处理好,上线后用户一多就掉线或收不到消息。
gorilla/websocket 和 github.com/gofiber/contrib/websocket 选哪个?
直接用 github.com/gofiber/contrib/websocket。它封装了 gorilla/websocket 的底层细节,把连接升级、错误拦截、上下文透传都集成进 Fiber 生命周期里。而裸用 gorilla/websocket 时,你得自己写中间件做 CheckOrigin、手动解析 header、还要在 websocket.Conn 失去请求上下文后想办法传用户信息——这些 Fiber 已经帮你兜住了。
常见错误现象:
- 用
gorilla/websocket单独起 HTTP handler,结果ctx.Locals在 WebSocket 回调里全为空 - 跨域请求被拦截,但没在
Upgrader.CheckOrigin里返回true,导致前端报400 WebSocket handshake error
实操建议:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 必须在
app.Use("/ws", ...)中提前校验 Origin 或 Token,再调用c.Next() - 不要在
websocket.New(...)闭包里访问c.Get("Authorization")——此时 header 已不可读,改用c.Locals("token")(需在中间件中提前存入)
如何安全地把用户身份绑定到每个 WebSocket 连接?
不能靠前端传的 uid 字段做唯一标识,更不能直接塞进 URL 参数(易伪造、无状态、难审计)。正确做法是在 Upgrade 阶段完成鉴权,并把可信字段存进 c.Locals,再透传给 *websocket.Conn 实例。
使用场景:
- 需要按用户 ID 区分发送/接收范围(比如私聊)
- 需要按群组 ID 路由广播(比如只推送给某团队成员)
- 需要记录在线状态并持久化(如写入 Redis)
实操建议:
- 在中间件中解析 Authorization Header 或 Cookie,验证 JWT 后提取
user_id和group_id,存为c.Locals("user_id")和c.Locals("group_id") - 把
c.Locals数据打包成结构体,在websocket.New闭包开头就取出来,别等读消息时才查 - 避免在
for { mt, msg, err := c.ReadMessage() }循环里反复调用数据库或 Redis 查询用户信息
消息广播为什么总漏发或 panic?
根本原因是多个 goroutine 并发读写共享 map 或 channel 时没加锁,或者往已关闭的 send channel 发消息。典型表现是日志里突然出现 panic: send on closed channel,或者部分客户端收不到上线通知。
性能与兼容性影响:
- 用
sync.Map替代原生map可避免读写冲突,但无法解决遍历时连接已断开的问题 - 广播前对每个 client 做
select { case c.send 非阻塞判断,比直接写更稳 - 不建议用全局
chan []byte做广播通道——一旦某个 client 写失败卡住,整个 channel 就堵死
实操建议:
- 每个
*Client维护独立send chan []byte,写失败时 close channel 并从 clients 集合中移除 - 广播逻辑里用
mutex.Lock()包住 map 遍历,且每次select写操作后立即mutex.Unlock(),别锁太久 - 在
Client.write()goroutine 里用defer close(c.send)确保资源清理
如何让聊天室支持多房间(Group)隔离?
单 Hub 模式(所有连接共用一个 broadcast channel)只能做全局广播,没法实现「A 房间发言,B 房间看不到」。必须把连接按 group_id 分桶管理,每个 group 对应一个独立的 Hub 实例或 map 子集。
容易踩的坑:
- 用
map[string]map[*Client]bool存 clients,但没对每个 group 的子 map 加锁,导致并发写 panic - 用户切换房间时只删旧 group 的 entry,却忘了关掉旧的
writegoroutine,造成 goroutine 泄漏 - 广播时遍历 group map,但没跳过已断开的连接,导致
WriteMessage报错后未处理,后续消息全卡住
实操建议:
- 用
sync.Map存group_id → *GroupHub,每个*GroupHub内部用sync.RWMutex + map[*Client]bool - 用户加入新 group 前,先调用旧
*GroupHub.Unregister(c),确保 write goroutine 退出 - 广播前统一检查
c.conn != nil && c.conn.IsOpen(),避免对已关闭连接调用WriteMessage
最常被忽略的一点:WebSocket 连接断开时,defer conn.Close() 不等于资源释放完成——你得主动从 group map 中删除它,并 close 对应的 send channel,否则残留的 goroutine 会持续尝试向已关闭的 channel 发消息,最终拖垮整个服务。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










