聊天室用mediator而非直接广播,是为了避免强耦合、统一消息分发逻辑、解耦用户状态与路由规则;它需线程安全、连接存活检查、非阻塞写入,并严格隔离职责——不存历史、不管连接生命周期、不掺和业务规则。

为什么聊天室不用直接广播而要用 Mediator
因为直接让每个 User 持有其他所有 User 的引用,会导致对象间强耦合:加人、踢人、私聊逻辑全得手动遍历、判空、同步状态。一旦要支持「房间隔离」「消息审计」「在线状态推送」,代码立刻散落在各处,改一处漏三处。
用 Mediator 把路由、过滤、分发逻辑收口到一个结构体里,User 只需调用 mediator.Broadcast() 或 mediator.SendTo(),不关心谁在线、谁掉线、谁被禁言。
常见错误现象:panic: send on closed channel —— 很多实现把 chan []byte 直接暴露给用户,没做发送前的连接存活检查;或者用 map 存用户却忘了加锁,高并发下 concurrent map writes。
-
Mediator必须持有*sync.RWMutex,读用户列表用RLock(),增删用Lock() - 每个
User应该带唯一id string和conn net.Conn,不要用指针地址当 ID - 广播前必须检查
conn.SetWriteDeadline()并捕获io.ErrClosed,主动清理失效连接
Mediator.Broadcast() 怎么避免阻塞主线程
如果所有消息都同步写进每个用户的 conn.Write(),一个慢连接(比如弱网手机)会拖垮整个广播——后续消息卡在 goroutine 里排队,新用户接入延迟飙升。
立即学习“go语言免费学习笔记(深入)”;
正确做法是为每个 User 启一个独立 goroutine 做写操作,并配超时控制:
go func(u *User, msg []byte) {
u.conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
if _, err := u.conn.Write(msg); err != nil {
mediator.Remove(u.id) // 主动下线
}
}(user, message)性能影响很实际:100 人房间,单次广播开 100 个 goroutine 看似吓人,但 Go runtime 调度开销极低;相比阻塞式广播导致的连接积压和内存暴涨,这是更稳的选择。
- 别用
sync.WaitGroup等全部写完——广播本就不需要强一致性 - 写失败后,
Remove()必须加锁,且要避免在循环中直接从正在遍历的 map 删除 - 如果消息体很大(如含图片 base64),先做序列化压缩再投递,否则 goroutine 堆积吃光内存
私聊和系统通知怎么复用同一套 Mediator
Mediator 不该只干“群发”一件事。它的核心价值是统一消息入口,所以 SendTo()、Announce()、Kick() 都应走同一个分发管道,只是路由规则不同。
例如系统通知(如「xxx 加入房间」)应该广播给除 sender 外所有人;私聊则只推给目标 id;而管理员指令可能还要校验 u.Role == "admin"。
- 所有方法最终都调用内部
deliver(msg Message, targets []string),复用连接检查和写逻辑 -
Message结构体必须含Type string字段(如"chat"/"join"/"kick"),前端靠它做 UI 分支 - 别在
SendTo()里重复写 conn.Write —— 提取成私有writeConn(conn net.Conn, msg []byte),避免 write deadline、error 处理逻辑散落
为什么 Mediator 不该保存消息历史
有人把 Mediator 当成消息中心,往里面塞 []Message 做缓存,结果上线三天内存涨到 2GB——这不是中介者模式的问题,是职责错位。
Mediator 只管“此刻怎么送”,不管“之前送过什么”。历史记录属于领域状态,该交给单独的 MessageStore(比如基于 LRU cache 或 Redis),并由上层业务决定是否回溯、存多久、谁有权查。
容易踩的坑:Mediator 里用 slice append 积累消息,没设上限;或用 map[string][]Message 按房间存,但忘记定时清理过期房间。
- 如果真要临时缓存(如新用户加入时补发最后 10 条),用带容量限制的
container/list,每次PushBack()后检查长度,超了就Remove(Front()) - 缓存内容必须是深拷贝后的
[]byte,不能存指向原始请求 body 的指针,否则 GC 收不走 - 任何涉及时间窗口的逻辑(如“最近 5 分钟消息”)必须用单调时钟
time.Now().UnixMilli(),别用time.Since()防止 NTP 校时翻车
事情说清了就结束。真正难的不是写出 Mediator,而是守住它的边界——不碰存储、不代管连接生命周期、不掺和业务规则。越往后迭代,这点越关键。










