用 sync.map 存连接更安全高效:需用 store/delete 操作、setclosehandler 清理、atomic 计数器统计在线人数,并通过心跳机制确保连接有效性。

用 sync.Map 存连接比用普通 map 安全得多
直接在全局 map[string]*websocket.Conn 上增删查,不加锁会 panic:并发写入 map 是 Go 运行时明确禁止的。用 sync.Mutex 包一层也能行,但读多写少场景下,sync.Map 更轻量、无锁读取更快,也更贴合“用户上线/下线频次低,查询在线数频次高”的实际模式。
实操建议:
- 声明为
var clients sync.Map,key 用用户 ID(如string),value 用*websocket.Conn或包装结构体 - 上线时用
clients.Store(userID, conn),不是clients.LoadOrStore—— 后者在已存在时不会覆盖,而重连可能需要更新连接句柄 - 下线必须调用
clients.Delete(userID),别只 close conn 就完事,否则统计永远不准 - 注意:如果 value 是结构体指针,
sync.Map不会深拷贝,安全;但如果存的是 struct 值类型,且后续要修改字段,得自己加锁或改用指针
conn.SetCloseHandler 比 defer close 更可靠
很多人在 WebSocket handler 里写 defer conn.Close(),以为能兜底清理。但连接异常断开(如客户端 kill 进程、网络闪断)时,Go 服务端未必能及时触发 defer,导致 sync.Map 里残留 dead conn,统计虚高。
实操建议:
- 在
conn建立后立刻注册关闭回调:conn.SetCloseHandler(func(code int, text string) error { clients.Delete(userID); return nil }) - 同时保留
defer conn.Close(),用于正常流程收尾,但不依赖它做清理 - 如果用了
gorilla/websocket,记得在握手前设置Upgrader.CheckOrigin和Upgrader.HandshakeTimeout,避免恶意连接耗尽资源 - 不要在 CloseHandler 里做耗时操作(如 DB 写入),它运行在 WebSocket 协议层 goroutine 中,阻塞会影响其他连接心跳
查在线人数别遍历 sync.Map,用原子计数器
clients.Range() 能拿到所有 key,但每次调用都要遍历整个 map,QPS 高时 CPU 明显上涨。而且 Range 返回的是快照,期间有新用户上线/下线,结果也不严格实时 —— 但在线人数本就允许秒级延迟,没必要强一致。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
实操建议:
- 单独维护一个
onlineCount int64,配合sync/atomic - 上线时:
atomic.AddInt64(&onlineCount, 1)+clients.Store(...) - 下线时:先
clients.Delete(...),再atomic.AddInt64(&onlineCount, -1)(顺序不能反,否则并发查到负数) - 查询接口直接返回
atomic.LoadInt64(&onlineCount),零分配、纳秒级响应 - 如果业务需要按角色/房间分组统计,就用多个
int64计数器,别试图用嵌套 map 实时聚合
WebSocket 连接没心跳?在线数很快就不准了
客户端休眠、路由器断开 NAT 映射、中间代理超时……这些都不会触发 CloseHandler。连接在服务端还开着,但实际已不可用。不主动探测,onlineCount 会越积越多。
实操建议:
- 服务端每 30 秒发一次
conn.WriteMessage(websocket.PingMessage, nil),客户端收到后必须回PongMessage - 设置
conn.SetPongHandler,并在里面更新该连接最后活跃时间(比如存在另一个sync.Map存时间戳) - 另起 goroutine 每分钟扫描一次,对超过 90 秒没 pong 的连接,手动
conn.Close()并触发清理逻辑 - 别用
conn.SetReadDeadline配合ReadMessage—— 它只管读,不管写和 ping/pong,容易漏判
最麻烦的不是怎么存连接,而是怎么定义“在线”:是 TCP 连接存活?还是能双向收发消息?还是用户界面显示为绿色小点?不同定义对应完全不同的检测逻辑和误差容忍度。别一上来就堆代码,先和产品对齐这个边界。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










