大家好,我是十三!欢迎来到十三Tech。
订单完成时要触发四件事——扣库存、发积分、推消息、统计积分。最初的代码是订单服务里直接调四个下游 client,每加一个下游就要改订单服务代码。测试时想 mock 一个下游都不行——四个 client 全得 mock 一遍。
后来抽了一个事件总线:订单完成时 bus.Publish("order.completed", order),下游各自订阅。订单服务只关心"订单完成"这件事,谁来响应由订阅者自己决定。新增下游零修改订单服务。
这就是观察者模式——Subject 维护订阅者列表,状态变化时自动通知所有订阅者。但很多人把观察者和责任链混在一起,因为都涉及"事件传递"。这一篇就聊聊观察者的标志特征是"单向通知",以及它和 Pub-Sub 的边界。
一、观察者在解决什么——不是"加回调"
教科书定义:定义对象间一对多的依赖,当一个对象状态变化时,所有依赖者得到通知。这话模糊——任何回调都可以套。真正的观察者有一个标志特征——单向通知。
"单向通知"指的是Subject 通知 Observer 后不关心 Observer 怎么处理。订单服务发布"订单完成"事件后,库存服务扣库存也好、消息服务推送也好、统计服务打点也好——Subject 一概不管。Subject 和 Observer 之间没有返回值链路。
这个特征是观察者区别于责任链、策略的核心。责任链是"链上传",传到某个节点就停;策略是"客户端选一个调";观察者是"广播给所有人,谁响应谁响应"。
判断口诀:一对多广播 + Subject 不关心响应 → 观察者;链上最多一个处理 → 责任链;客户端选一个 → 策略。
二、观察者怎么写——同步 vs 异步 vs Channel
Go 里观察者有三种典型实现,每种适用不同场景。
type Event struct {
Name string
Data interface{}
}
type Observer interface { Update(e *Event) }
type EventBus struct {
mu sync.RWMutex
observers []Observer
}
func (b *EventBus) Subscribe(o Observer) {
b.mu.Lock(); defer b.mu.Unlock()
b.observers = append(b.observers, o)
}
func (b *EventBus) Publish(e *Event) {
b.mu.RLock(); defer b.mu.RUnlock()
for _, o := range b.observers { go o.Update(e) }
}
这段代码用了 goroutine 异步通知——避免慢订阅者阻塞 Subject。如果订阅者都是轻量操作(缓存、计数),同步通知也行;如果订阅者要做 DB 写、HTTP 调用,必须异步。
更 Go 风的写法是用 channel——Subscribe 返回 <-chan *Event,订阅者从 channel 读。channel 天然支持超时、取消、并发安全,比 interface 实现优雅。
写观察者有个工程坑:订阅者异常要隔离。一个订阅者 panic 不能影响其他订阅者——同步通知时一定要 defer recover(),异步通知时 goroutine 里也要 recover。否则一个 bug 把整个事件总线搞挂,所有订阅者都拿不到事件。
三、什么时候别用观察者
观察者不是所有"事件驱动"场景都适用,至少有三个反模式。
反模式一:订阅者需要返回结果。观察者是单向通知——Subject 发完就完,不管订阅者返回什么。如果你发现 Subject 在等订阅者的返回值(比如"扣库存失败就回滚订单"),那不是观察者,是同步调用或责任链。
反模式二:订阅者必须按顺序处理。观察者的通知顺序未定义——不能假设"先通知 A 再通知 B"。如果业务需要严格顺序(先扣库存再发积分),用责任链或者干脆直接调用。
反模式三:订阅者超过 20 个。一个事件被二十多个订阅者监听——发布一次要触发二十多次回调,慢得离谱。这种规模应该用专业消息队列(Kafka、NATS),而不是进程内观察者。
Go 标准库里观察者最经典的地方是 context.Context——一个 context 被 cancel 时,所有从它派生的子 context(及监听 Done() 的 goroutine)都收到通知。context.WithCancel(parent) 创建的 ctx 跟 parent 形成观察者关系——parent cancel 时,ctx.Done() channel 关闭,所有 <-ctx.Done() 的 goroutine 立刻收到。看 net/http 的 request context 实现——server 处理完请求后 cancel request context,所有派生的 DB query、HTTP call 都立刻收到取消信号。这就是观察者在标准库里的教科书用法。
回头看那个订单完成场景——值得用观察者吗?值得,四个下游、彼此独立、新增频繁,正是观察者的甜区。但如果只是"扣库存失败就回滚订单"这种强一致需求,同步调用更直接,观察者反而埋雷。
观察者真正教会我的,不是"一对多通知",而是区分"关心发生什么"和"关心怎么响应"——Subject 关心前者,Observer 关心后者,两者解耦让系统更柔韧。这个区分比模式本身重要得多。
下一篇讲模板方法模式,它解决的是"算法骨架固定、步骤可变"的问题。
关于十三Tech 资深服务端研发,AI 实践者,专注分享真实可落地的技术经验。 相信 AI 是程序员的最佳搭档。
联系方式:569893882@qq.com GitHub:@TriTechAI
