观察者模式优于hub广播,因其按房间隔离订阅、支持动态扩缩容且查询为o(1);room需含observers map[*client]bool、sync.rwmutex及带错误处理的broadcast方法。

为什么不用 Hub 广播而要上观察者模式
Hub 模式适合全站广播,但一进多房间(比如 /room/a、/room/b)就硬编码或靠字符串前缀路由,很快会失控:消息误投、房间状态难隔离、扩缩容时 map 锁竞争加剧。观察者模式把“谁该收哪条消息”从 Hub 的遍历逻辑里解耦出来,让每个房间自己维护订阅者,天然支持动态创建/销毁。
- 场景明确:用户连接时带
room_id参数,只订阅对应房间;断开时自动退订,不污染其他房间 - 性能差异:Hub 全量遍历
clients是 O(n),观察者按频道查observers[room_id]是 O(1) - 坑点提示:
map[String]*Room必须用sync.RWMutex保护,否则并发注册房间时 panic:”fatal Error: concurrent map writes“
Room 结构体怎么设计才不漏状态
Room 不是单纯存 client 列表,它得管三件事:谁在、谁要收、谁掉线了。漏掉任意一个,就会出现“人还在界面上但收不到消息”或“已离线用户还占着内存”。
- 必须包含
observers map[*Client]bool(用指针作 key,避免 copy 副本) - 必须带
mu sync.RWMutex—— 所有对observers的读写都走mu.Lock()/mu.RLock() - 必须提供
Broadcast(message []byte)方法,内部先mu.RLock(),再遍历发送;失败时触发Unregister而非静默丢弃 - 示例片段:
func (r *Room) Broadcast(msg []byte) { r.mu.RLock() defer r.mu.RUnlock() for client := range r.observers { select { case client.send <- msg: default: // 写失败 → 客户端可能已断,清理 r.Unregister(client) } } }
Client 怎么安全注册到多个 Room
一个用户可能同时在「技术讨论」和「摸鱼闲聊」两个房间,Client 实例不能只挂在一个 Room.observers 里,得支持多房间注册,且注销时不能误删其他房间的引用。
- 每个
Client维护rooms map[string]*Room,记录自己所属的所有房间 -
Register(room *Room, roomID string)方法里:先room.Register(c),再存入c.rooms[roomID] = room -
Unregister(roomID string)方法里:先从room.observers删除自己,再从c.rooms删除该roomID键 —— 顺序反了会导致残留 - 关键检查:http 升级时解析 URL 参数用
url.ParseQuery(r.URL.RawQuery),别直接用r.URL.Query().Get("room"),后者对重复参数或编码字符处理不可靠
消息路由时最容易卡死的 channel 场景
观察者模式依赖 channel 传递事件,但 client.send 是带缓冲的 chan []byte,缓冲区设太小或客户端网络抖动,就会堵住整个 Room.Broadcast 流程,连带拖慢其他房间。
立即学习“go语言免费学习笔记(深入)”;
- 缓冲大小建议设为
64或128,别用0(无缓冲)或过大值(OOM 风险) - 发送时必须用
select+default,不能直写client.send ,否则 <a style="color:#f60; text-decoration:underline;" title="go" href="https://www.php.cn/zt/15863.html" target="_blank">go</a>routine 永久阻塞 - 更稳做法:在
writePump里加超时控制,比如case 后主动 close <code>client.send - 典型错误现象:
goroutine stack size exceeded日志频出,本质是 writePump 协程卡死,新协程不断 fork 出来抢资源
观察者模式本身不难,难的是所有 Register/Unregister 调用点必须成对、所有 mu 锁范围必须精确覆盖 map 操作、所有 channel 发送必须带 fallback 机制——少一个,上线三天后准出问题。