最稳妥方案是用 sync.map 或原子变量+通道组合管理在线状态,因普通 map 并发读写会 panic,加锁也易漏锁、阻塞广播或向关闭 channel 发送消息;sync.map 适合高频查询但不支持原子遍历,广播需另起 goroutine;用户连接应由专属 goroutine 管理,通过 channel 通信,实现状态变更与消费彻底分离。

用 sync.Map 或原子变量 + 通道组合管理在线状态最稳妥,直接用普通 map 加 sync.RWMutex 在高并发读写下容易卡住或漏广播。
为什么不能直接用普通 map 存在线用户
常见错误是定义 var onlineMap map[string]User 后,在多个 goroutine 中并发读写 —— 这会直接 panic:"fatal error: concurrent map read and map write"。即使加了 sync.RWMutex,也容易踩两个坑:
- 忘记在
for range遍历前加读锁,或遍历时锁被提前释放 - 广播消息时遍历
map和写入conn.Write混在同一 goroutine,一旦某个客户端连接卡住(比如网络抖动),整个广播循环就阻塞,其他用户收不到消息 - 用户下线时只删
map但没关掉其专属chan string,导致Manager向已关闭 channel 发送消息而 panic
sync.Map 适合只读频繁、写少的在线列表场景
sync.Map 对读操作无锁,比 map + RWMutex 在大量并发查询(如“查某人是否在线”)时更轻量。但它不支持原子性遍历 —— 你无法安全地“获取所有 key”再逐个发消息。所以它只适合做状态快照或索引,不能替代广播逻辑:
在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。
- 上线时用
onlineMap.Store(addr, user) - 下线时用
onlineMap.Delete(addr) - 但广播必须另起 goroutine,从
messagechannel 拿到消息后,再用onlineMap.Range遍历(注意:Range是快照,期间新增用户不会被包含) - 别对
sync.Map做len()统计,它不提供长度接口;真要统计人数,单独用atomic.Int64计数
用有状态 goroutine 封装用户状态最干净
把每个用户的连接生命周期、消息收发、心跳检测全交给一个专属 goroutine 管理,外部只通过 channel 通信。这样避免锁、避免 channel 关闭混乱、也天然隔离错误:
- 每个
User结构体带一个msgChan chan *Message,只由其专属 goroutinehandleUserConn读取 - 全局
Manager不直接操作map,而是往每个用户的msgChan发消息;用户 goroutine 自己决定怎么写、写失败是否重试、超时是否下线 - 心跳检测也放在这里:
select中加time.After(30 * time.Second)分支,超时未收到新消息就主动 close(msgChan) 并退出 goroutine - 上线/下线广播统一走一个
broadcastChan chan BroadcastEvent,由独立broadcastergoroutine 处理,和用户连接 goroutine 完全解耦
原子变量比互斥锁更适合开关类状态
像 “用户是否已断开”“连接是否正在关闭” 这类布尔状态,用 atomic.Bool 比 sync.Mutex 更轻量且无死锁风险:
- 连接关闭时:
if conn.closed.CompareAndSwap(false, true) { conn.netConn.Close() },确保只关一次 - 心跳响应中检查:
if !conn.alive.Load() { return },避免向已标记断开的连接发包 - 不要用
atomic.Value存结构体指针来模拟“可变状态”,它只保证写入/读取原子,不保证结构体内字段修改的原子性;真要存复杂状态,还是走有状态 goroutine
真正难的不是存在线用户,而是让“状态变更”和“状态消费”彻底分离 —— 上线事件、消息广播、心跳超时、连接异常关闭,这些动作触发源不同、时效要求不同、失败影响范围也不同。混在一个 goroutine 里处理,早晚会因一个慢连接拖垮整个服务。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










