Golang - 观察者模式(Observer Pattern)

观察者模式(Observer Pattern)

一、模式定义

观察者模式定义了对象之间一对多的依赖关系:当一个对象(被观察者 / Subject)的状态发生变化时,所有依赖于它的对象(观察者 / Observer)都会收到通知并自动更新。

打个比方:你关注了某个 UP 主,UP 主发新视频时你会收到推送。UP 主不需要认识每一个粉丝,只需要在发布时"广播"一下,关注了的人自然就收到了。

二、解决什么问题

  • 耦合问题:如果一个对象的状态变化需要直接调用 N 个对象的更新方法,修改任何一个被调用方都会影响主体代码
  • 动态订阅:运行时需要灵活地增减观察者,而不是在编译期就固定下来
  • 广播通信:一个事件需要通知多个不相关的模块(日志、缓存、UI 刷新等),但事件源不应该知道这些模块的存在

三、核心角色

角色职责
Subject(被观察者)维护观察者列表,提供注册/注销/通知方法
Observer(观察者接口)声明更新方法,所有具体观察者实现此接口
ConcreteSubject持有具体状态,状态变化时调用 Notify 触发通知
ConcreteObserver实现更新逻辑,从 Subject 获取自己需要的数据

四、结构图

┌─────────────────────┐       ┌──────────────────────┐
│    Subject          │       │     Observer         │
├─────────────────────┤       ├──────────────────────┤
│ - observers []Obs   │       │ + Update(data)       │
│ + Attach(o)         │<──────│                      │
│ + Detach(o)         │       └──────────┬───────────┘
│ + Notify()          │                  │ implements
│ + GetState() State  │       ┌──────────┴───────────┐
└─────────┬───────────┘       │ ConcreteObserver     │
          │ inherits          │ - name string        │
┌─────────┴───────────┐       │ + Update(data)      │
│ ConcreteSubject     │       └──────────────────────┘
│ - state string      │
│ + SetState(s)       │
└─────────────────────┘

五、Go 实现要点

Go 实现观察者模式有两个关键设计决策:

1. 推模式 vs 拉模式

  • 推模式:Subject 在 Notify 时直接把变化的数据推给观察者。优点是观察者不用反查 Subject;缺点是不同观察者可能只需要部分数据
  • 拉模式:Notify 只通知"有变化了",观察者自行调用 Subject.GetState() 获取所需数据。优点是按需取用;缺点是多一次调用

实际项目中推模式更常见,因为通知时 Subject 的状态可能已经变了,拉模式可能拿到的是最新状态而非触发通知时的状态。

2. 线程安全

Go 中观察者列表的增删和通知通常并发进行,必须用 sync.Mutexsync.RWMutex 保护。注意死锁陷阱:在持锁状态下调用观察者的 Update 方法,如果 Update 反过来操作 Subject 就会死锁。解决方案是先拷贝观察者列表,释放锁后再遍历通知

六、完整练习代码

package main

import (
	"fmt"
	"sync"
)

// ============================================================
// 观察者接口
// ============================================================

// Event 携带事件数据,推模式下由 Subject 传给 Observer
type Event struct {
	Topic   string      // 事件主题标识
	Payload interface{} // 事件携带的数据
}

// Observer 观察者接口,任何实现了 OnEvent 的类型都可以作为观察者
type Observer interface {
	OnEvent(event Event)
}

// ============================================================
// 被观察者(Subject)
// ============================================================

// Subject 管理观察者列表,提供注册/注销/通知能力
type Subject struct {
	mu         sync.RWMutex
	observers  []Observer // 观察者列表
	nextID     int        // 观察者ID分配器,用于注销时定位
	observerID map[Observer]int
}

func NewSubject() *Subject {
	return &Subject{
		observerID: make(map[Observer]int),
	}
}

// Attach 注册观察者,返回观察者ID用于后续注销
func (s *Subject) Attach(o Observer) int {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.nextID++
	s.observers = append(s.observers, o)
	s.observerID[o] = s.nextID
	return s.nextID
}

// Detach 注销观察者
func (s *Subject) Detach(o Observer) {
	s.mu.Lock()
	defer s.mu.Unlock()
	for i, obs := range s.observers {
		if obs == o {
			s.observers = append(s.observers[:i], s.observers[i+1:]...)
			delete(s.observerID, o)
			return
		}
	}
}

// Notify 广播事件给所有观察者
// 关键:先在锁内拷贝观察者列表,释放锁后再调用 OnEvent
// 这样即使 OnEvent 回调中操作 Subject(如 Attach/Detach)也不会死锁
func (s *Subject) Notify(event Event) {
	s.mu.RLock()
	snapshot := make([]Observer, len(s.observers))
	copy(snapshot, s.observers)
	s.mu.RUnlock()

	for _, o := range snapshot {
		o.OnEvent(event)
	}
}

// ============================================================
// 具体观察者实现
// ============================================================

// 日志观察者:记录所有事件
type LoggerObserver struct {
	name string
}

func NewLoggerObserver(name string) *LoggerObserver {
	return &LoggerObserver{name: name}
}

func (l *LoggerObserver) OnEvent(event Event) {
	fmt.Printf("[日志-%s] 收到事件: topic=%s, payload=%v\n",
		l.name, event.Topic, event.Payload)
}

// 邮件观察者:只关注特定主题
type EmailObserver struct {
	interestedTopic string
}

func NewEmailObserver(topic string) *EmailObserver {
	return &EmailObserver{interestedTopic: topic}
}

func (e *EmailObserver) OnEvent(event Event) {
	if event.Topic == e.interestedTopic {
		fmt.Printf("[邮件] 发送告警邮件: %v\n", event.Payload)
	}
}

// 统计观察者:统计事件数量
type MetricsObserver struct {
	mu    sync.Mutex
	count map[string]int
}

func NewMetricsObserver() *MetricsObserver {
	return &MetricsObserver{
		count: make(map[string]int),
	}
}

func (m *MetricsObserver) OnEvent(event Event) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.count[event.Topic]++
	fmt.Printf("[统计] 事件 %s 累计触发 %d 次\n", event.Topic, m.count[event.Topic])
}

// ============================================================
// 具体被观察者:订单服务
// ============================================================

type OrderService struct {
	subject *Subject
}

func NewOrderService() *OrderService {
	return &OrderService{subject: NewSubject()}
}

// Subscribe 让外部注册观察者
func (os *OrderService) Subscribe(o Observer) int {
	return os.subject.Attach(o)
}

// Unsubscribe 注销观察者
func (os *OrderService) Unsubscribe(o Observer) {
	os.subject.Detach(o)
}

// CreateOrder 下单后触发事件链
func (os *OrderService) CreateOrder(orderID string, amount float64) {
	fmt.Printf("\n>>> 创建订单: %s, 金额: %.2f\n", orderID, amount)

	// 1. 推送订单创建事件
	os.subject.Notify(Event{
		Topic:   "order.created",
		Payload: fmt.Sprintf("订单号=%s, 金额=%.2f", orderID, amount),
	})

	// 2. 如果金额超过阈值,触发风控事件
	if amount > 10000 {
		os.subject.Notify(Event{
			Topic:   "order.risk_alert",
			Payload: fmt.Sprintf("大额订单告警: %s, 金额=%.2f", orderID, amount),
		})
	}
}

// ============================================================
// 主函数:演示观察者模式
// ============================================================

func main() {
	// 创建订单服务(被观察者)
	orderService := NewOrderService()

	// 创建并注册观察者
	logger := NewLoggerObserver("系统日志")
	emailAlert := NewEmailObserver("order.risk_alert") // 只关心风控事件
	metrics := NewMetricsObserver()

	orderService.Subscribe(logger)
	orderService.Subscribe(emailAlert)
	orderService.Subscribe(metrics)

	fmt.Println("========== 第一笔订单(小额)==========")
	orderService.CreateOrder("ORD-001", 199.00)

	fmt.Println("\n========== 第二笔订单(大额,触发风控)==========")
	orderService.CreateOrder("ORD-002", 58000.00)

	fmt.Println("\n========== 注销日志观察者后 ==========")
	orderService.Unsubscribe(logger)
	orderService.CreateOrder("ORD-003", 888.00)

	fmt.Println("\n========== 最终统计 ==========")
	fmt.Printf("各主题事件计数: %+v\n", metrics.count)
}

七、运行结果预期

========== 第一笔订单(小额)==========
>>> 创建订单: ORD-001, 金额: 199.00
[日志-系统日志] 收到事件: topic=order.created, payload=订单号=ORD-001, 金额=199.00
[统计] 事件 order.created 累计触发 1 次

========== 第二笔订单(大额,触发风控)==========
>>> 创建订单: ORD-002, 金额: 58000.00
[日志-系统日志] 收到事件: topic=order.created, payload=订单号=ORD-002, 金额=58000.00
[统计] 事件 order.created 累计触发 2 次
[日志-系统日志] 收到事件: topic=order.risk_alert, payload=大额订单告警: ORD-002, 金额=58000.00
[邮件] 发送告警邮件: 大额订单告警: ORD-002, 金额=58000.00
[统计] 事件 order.risk_alert 累计触发 1 次

========== 注销日志观察者后 ==========
>>> 创建订单: ORD-003, 金额: 888.00
[统计] 事件 order.created 累计触发 3 次

========== 最终统计 ==========
各主题事件计数: map[order.created:3 order.risk_alert:1]

八、关键设计决策解析

1. 为什么 Notify 要先拷贝再通知

如果在持锁状态下调用 OnEvent,而观察者内部又操作 Subject(比如某个观察者收到事件后注册新观察者),就会造成 sync.RWMutex 的重入问题导致死锁。拷贝列表后释放读锁,通知阶段不再持有锁,观察者可以安全地操作 Subject。

2. Event 结构体设计

Topic + Payload 的事件模型比直接传具体数据更灵活。观察者通过 Topic 过滤自己关心的事件,通过 Payload 获取数据。这本质上就是推模式——Subject 把变化推给所有观察者,观察者自行决定是否处理。

3. 与发布订阅模式的区别

维度观察者模式发布订阅模式
通信方式Subject 直接调用 Observer通过 Event Bus / Broker 中转
耦合度Subject 持有 Observer 引用发布者和订阅者互不感知
Go 实现接口 + 切片channel 通信
适用场景进程内事件通知跨模块/跨进程消息分发

九、实际项目身影

  • database/sql 连接池:连接状态变化通知
  • Kubernetes 控制器:Watch 资源变更事件,Reconcile 循环响应
  • Vue/React 响应式系统:数据变更 → 视图自动更新(本质就是观察者)
  • Go 的 context.WithCancel:取消信号传播本质上也是观察者模式——父 context 取消时通知所有子 context

十、小结

观察者模式是解耦"事件源"和"事件处理"的经典手段。在 Go 中实现时,核心要点有三个:用接口定义观察者契约、用互斥锁保护观察者列表、用"先拷贝后通知"避免死锁。当观察者数量多且需要跨模块通信时,可以进一步演进为基于 channel 的发布订阅模式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值