在Go语言项目中,随着业务增长,模块之间常常因为直接函数调用而纠缠不清。假设订单服务完成之后要通知库存、短信和日志三个系统,若采用同步调用,订单模块就必须引入其他三方的接口,任何一方的签名变动都会影响订单代码。观察者模式把这种关系反转:订单只负责把事件抛出来,关心该事件的模块自己注册接收逻辑,彼此不需要互相知道对方存在。

发布订阅是在观察者模式之上的进一步抽象。它加入了一个中间的代理角色,也就是消息总线。发布者把消息交给总线,订阅者从总线按需收取,双方生命周期完全独立。Go语言没有内建的发布订阅框架,但利用chan和sync.Map就能写出一个轻量实现。下面先从最基础的接口定义说起。
一、观察者模式的核心结构与Go实现
观察者模式的本质是定义一对多的依赖关系。在Golang中,我们通常用接口来表述观察者,用结构体保存观察者列表。定义一个Observer接口,仅包含一个Update方法,任何需要响应事件的类型只要实现它即可被纳入通知体系。这种基于接口的抽象让编译器只校验行为,而不关心具体类型,从而切断了模块间的强引用。
下面是一个最简观察者实现。主题对象维护一个切片存放观察者,在状态变化时遍历调用。注意这里没有使用锁,如果在并发环境注册和通知,需要配合sync.Mutex保护切片。该写法展示了如何把“调用别人”变成“别人来登记,我统一喊一声”的模型。
package main
import "fmt"
// Observer 观察者接口
type Observer interface {
Update(msg string)
}
// Subject 主题
type Subject struct {
observers []Observer
}
func (s *Subject) Register(o Observer) {
s.observers = append(s.observers, o)
}
func (s *Subject) Notify(msg string) {
for _, o := range s.observers {
o.Update(msg)
}
}
// EmailObserver 具体观察者
type EmailObserver struct{}
func (e EmailObserver) Update(msg string) {
fmt.Println("邮件系统收到:", msg)
}
func main() {
sub := &Subject{}
sub.Register(EmailObserver{})
sub.Notify("订单创建成功")
}
这种基础结构已经能解耦调用方和响应方,但所有观察者都收同一类消息,且主题必须持有观察者实例。若系统跨进程或需要按消息类型过滤,基础观察者就不够用了,这时发布订阅的引入变得必要。
二、发布订阅总线的设计思路
发布订阅的关键是把“主题”升级为“总线”或“代理”。发布者不再认识观察者,只把消息发到某个主题字符串上;总线内部维护主题到订阅者队列的映射,收到消息后转发给匹配队列。Go里可以用sync.Map存主题和chan的对应关系,每个订阅者拥有自己的channel,由总线推送,避免回调阻塞发布者。
下面的示例展示了如何用Go写一个简单的发布订阅总线。它支持Subscribe返回接收通道,也支持Publish向某主题广播。由于channel自带阻塞和并发安全特性,我们少写了很多锁代码。不过要注意,如果订阅者处理慢,channel满会导致发布者卡住,所以生产环境常用带缓冲的channel或起独立goroutine分发。
package main
import (
"sync"
)
type Bus struct {
topics sync.Map // map[string][]chan string
}
func (b *Bus) Subscribe(topic string) <-chan string {
ch := make(chan string, 10)
val, _ := b.topics.LoadOrStore(topic, []chan string{})
list := val.([]chan string)
list = append(list, ch)
b.topics.Store(topic, list)
return ch
}
func (b *Bus) Publish(topic string, msg string) {
if val, ok := b.topics.Load(topic); ok {
for _, ch := range val.([]chan string) {
ch <- msg
}
}
}
通过总线,订单模块只依赖Bus的Publish方法,短信模块只依赖Subscribe。新增一个审计模块,只需订阅原有主题,订单代码零修改。这就是发布订阅在架构层面带来的扩展自由,也是它相比单纯观察者更适应复杂系统的原因。
三、解耦效果与工程落地注意事项
从依赖方向看,观察者模式消除了上游对下游的类型依赖,发布订阅连运行实例依赖也消除了。在Go的单体服务中,这能显著降低包引用环的出现概率。比如把order包和sms包都依赖bus包,而不是互相依赖,编译图从网状变成星状,重构时心理负担小很多。
但解耦不是免费午餐。引入总线后,调试链路变长,一个问题不再停留在调用栈里,而是散落在消息流中。建议在消息体里带上trace_id,并在总线层做统一日志。另外,若某订阅者panic,可能影响同一个goroutine里的后续推送,因此每个订阅消费建议用defer recover包一层,或者让订阅者自己起goroutine读channel,把故障隔离在自身范围内。
最后要考虑消息可靠性。上面例子是内存级总线,进程重启消息就丢。若业务不允许丢,应把总线换成基于Redis或Kafka的外部组件,Go客户端只需改Bus的实现,上层发布订阅代码不变。这种“换底层不影响上层”的能力,正是良好解耦设计最实在的收益。
observer_patternpublish_subscribeGolang修改时间:2026-08-13 09:10:04