Go中实现中介者模式的核心是用接口+组合控制依赖关系:User只持Mediator接口,不直接引用其他User;中介者统一处理转发逻辑,便于扩展审计、限流等功能,避免强耦合与重复代码。

Go 里实现中介者模式,核心不是“写个 ChatRoom 就完事”,而是用接口 + 组合把“谁该知道谁”这件事控制住——用户不持有其他用户,只持有一个 Mediator 接口;中介者持有所有用户指针,但用户之间完全无引用。
为什么不能让 User 直接调用另一个 User 的 Receive?
直接调用意味着强耦合:新增一个用户类型(比如 BotUser)就得改所有发送逻辑;想加消息审计、限流、日志,得在每个 SendTo 里重复写。中介者模式把“转发决策”收归一处,后续加广播策略、私聊校验、离线缓存,都只动 ChatRoom.Send,不动任何 User。
- 常见错误:在
User.Send里硬编码遍历users列表 —— 这等于绕过中介者,模式失效 - 正确做法:
User只认Mediator接口,具体怎么发、发给谁,由实现该接口的*SimpleChatRoom决定 - 接口定义要窄:只暴露
Send(from, to, msg string),别塞BanUser或GetOnlineCount—— 那是业务逻辑,不是中介职责
Mediator 接口设计的关键取舍
Go 没有泛型约束(老版本)或泛型太重时,Mediator 接口参数用 string 还是 interface{}?实际项目中推荐字符串路由,轻量且易调试。
type Mediator interface {
Send(from, to, message string)
}
// 而非
// Send(event Event) —— Event 需定义结构体、序列化、反序列化,小项目纯属累赘
// Notify(sender interface{}, data interface{}) —— 类型断言满天飞,测试难覆盖
- 私聊场景:
to是用户名(string),查map[string]User即可,快且直观 - 广播场景:
to == ""或to == "all",统一走Broadcast分支 - 避免用
interface{}当万能参数:一旦中介者内部要做if v, ok := data.(UserAction); ok,就退化成 C 风格 void*,失去 Go 的类型安全优势
注册时机与空指针 panic 的真实来源
最常触发 panic 的不是并发,而是 User.Send 时 u.chatRoom 为 nil —— 因为忘了调 room.Register(u),或者注册发生在 User 初始化之后但 Send 调用之前。
立即学习“go语言免费学习笔记(深入)”;
- 防御写法:在
User.Send开头加if u.chatRoom == nil { log.Warn("user not registered"); return } - 更稳妥:用构造函数强制绑定,例如
NewChatUser(name, room),而不是先NewChatUser(name)再手动SetChatRoom - 不要依赖延迟注册:比如在 HTTP handler 里才注册用户,而 goroutine 已经开始发心跳 —— 竞态+panic 双杀
并发安全:为什么 map[string]User 不能裸用?
Register 和 Send 很可能被不同 goroutine 调用(如 WebSocket 连接建立 vs 消息接收),map 非并发安全,会直接 panic。
type SimpleChatRoom struct {
mu sync.RWMutex
users map[string]User
}
func (c *SimpleChatRoom) Register(user User) {
c.mu.Lock()
defer c.mu.Unlock()
user.SetChatRoom(c)
c.users[user.GetName()] = user
c.Broadcast("system", fmt.Sprintf("%s 加入了聊天室", user.GetName()))
}
func (c *SimpleChatRoom) Send(from, to, message string) {
c.mu.RLock()
defer c.mu.RUnlock()
// ... 查 map、转发逻辑
}
- 别用
sync.Map替代:它适合读多写少,但注册/下线是写密集操作,sync.RWMutex + map更可控 - 广播时别在锁内调
user.Receive:防止用户回调阻塞整个房间;应先收集目标用户切片,再解锁后遍历调用 - 如果用 channel 做消息队列(如
chan Message),注意缓冲区大小 —— 满了会 blockSend,影响实时性
中介者模式真正的复杂点不在代码行数,而在“边界感”:哪些逻辑必须塞进中介者(比如消息格式校验、用户状态检查),哪些必须留在用户侧(比如 UI 渲染、本地缓存)。一旦中介者开始处理渲染逻辑或调用数据库,它就不再是协调者,而是上帝对象 —— 后续谁都改不动。










