Go中无需手写Mediator模式,因其违背Go的简洁哲学;应优先使用channel、sync工具、包级封装和函数式回调来解耦,避免上帝对象和过度设计。

Go 里真没必要手写 Mediator 模式
Go 语言没有类继承、不鼓励对象间强耦合,更不提供接口多实现的“优雅抽象”陷阱。所谓 Mediator 模式在 Go 中既难落地,又容易变成过度设计——你写的不是解耦,是给自己加调度层。
常见错误现象:Mediator 实现后,所有通信都绕道一个中心结构体,结果这个结构体迅速膨胀成上帝对象,还带上了状态管理、事件分发、生命周期钩子……最后比原始耦合还难测、难改。
- Go 的
channel+select天然适合点对点或一对多通信,不需要中介者“转发” - 如果多个 goroutine 要协调状态,优先用
sync.Mutex、sync/atomic或context.Context传取消信号,而不是塞进中介者 - 想解耦模块?靠包级封装 + 明确的输入输出(比如函数参数只收
io.Reader,不收具体 struct)比模式更有效
什么时候你会误以为需要 Mediator
典型场景:几个 service(比如 UserSvc、OrderSvc、NotificationSvc)要互相通知状态变更。你开始想:“得有个中间人来管这些消息”。停一下——这其实是事件驱动的误用起点。
真实问题往往不是“通信太复杂”,而是“职责没切开”或“依赖没反转”。比如 OrderSvc 直接调 NotificationSvc.SendEmail(),这不是耦合,这是依赖倒置没做;应该让 OrderSvc 触发一个事件(如 OrderPlacedEvent),由外部订阅者决定怎么通知。
立即学习“go语言免费学习笔记(深入)”;
- 用
func(Order) error类型定义回调,比塞进Mediator.Register("order.placed", ...)更轻、更易测 - 若真需广播,用
chan OrderPlacedEvent配合多个go func(){...}()订阅,不用中介者调度 - 避免把 HTTP handler、DB repo、cache client 全塞进同一个
Mediator实例——它们本就不该知道彼此
如果你坚持要个“中介”结构体
那它只能是无状态的、纯函数式的协调器,不能持有业务数据,也不能参与决策逻辑。否则很快就会变成隐藏的全局状态源。
示例中这个 EventBroker 是可接受的底线:
type EventBroker struct {
subscribers map[string][]func(interface{})
}
func (b *EventBroker) Publish(topic string, data interface{}) {
for _, fn := range b.subscribers[topic] {
go fn(data) // 注意:不等执行完成,不处理 panic
}
}
func (b *EventBroker) Subscribe(topic string, fn func(interface{})) {
b.subscribers[topic] = append(b.subscribers[topic], fn)
}
- 它不保存订单、用户等业务对象,只存函数指针
- 不负责重试、顺序、事务一致性——那是上层逻辑的事
- 别给它加
Start()/Stop()方法,也不让它实现任何 interface;它就是个薄薄的注册+触发容器 - 测试时直接传入匿名函数断言是否被调,别 mock 它
真正降低通信复杂度的 Go 做法
不是引入新角色,而是减少角色间的可见性。Go 的包机制和导出规则就是天然的“通信防火墙”。
- 把共享数据定义在最下游包(如
model/),上游只依赖它,不反向依赖 - 用
interface{}参数接收行为,而不是传 struct 指针(比如Process(func() error)比Process(*Mediator)更松) - HTTP handler 里不做业务编排,只解析请求、调用 service、返回响应;编排逻辑放在单独的
usecase/包里,且不暴露给 handler - 警惕
init()函数、全局变量、包级 var —— 它们才是 Go 中真正的“隐式中介者”,而且无法单元测试
模式是描述已有结构的词汇,不是构建新结构的模具。Go 程序变复杂,通常是因为没守住包边界,或者过早抽象了还没稳定下来的交互流程。这时候加个 Mediator,就像给漏水的屋顶贴金箔。










