如何在Golang中实现观察者通知机制_Golang观察者模式事件派发示例

9次阅读

go语言可用map+sync.RWMutex+chan手动实现线程安全观察者模式:用RWMutex保护事件名到回调列表的映射,Notify异步执行并recover panic,Subscribe返回注销函数,注意函数相等性限制。

如何在Golang中实现观察者通知机制_Golang观察者模式事件派发示例

Go 语言没有内置的观察者模式支持,也没有类似 EventEmitter标准库类型,所以必须手动实现——但不需要第三方库,用 map + sync.RWMutex + chan 就能写出线程安全、低开销、可取消的通知机制。

如何用 mapsync.RWMutex 管理订阅者

核心是维护一个事件名到回调函数列表的映射。不能直接用 map[String][]func(Interface{}),因为并发读写会 panic;也不能只靠 sync.Mutex,读多写少场景下 RWMutex 更合适。

关键点:

  • 订阅时用 RWMutex.Lock(),避免写冲突
  • 通知时用 RWMutex.RLock(),允许多个 goroutine 并发读取监听器列表
  • 回调函数签名统一为 func(interface{}),保持事件数据类型灵活
  • 不预分配 slice 容量,避免误判订阅数量(实际可能动态增删)

为什么通知逻辑要避免阻塞发布者

如果在 Notify() 中同步执行所有回调,某个慢回调(比如 http 请求、日志落盘)会拖慢整个事件流,甚至导致调用方超时。更糟的是,若回调 panic,未 recover 会导致整个通知中断,后续监听器收不到事件。

立即学习go语言免费学习笔记(深入)”;

推荐做法是异步派发:

  • 每个事件通知启动独立 goroutine 执行回调,互不影响
  • recover() 捕获单个回调 panic,不传播到其他监听器
  • 不加 waitgroupchan 等待回调结束——发布者只负责“发出”,不关心“送达”

示例中 notifyAsync 方法就是按这个思路写的。

如何支持监听器动态注销和事件一次性消费

原生 map 删除键值对容易,但「注销指定回调」需要额外结构。常见错误是用闭包比较函数地址——Go 中函数变量不可比较,== 会编译失败。

可行方案有两种:

  • 返回注销函数(func()),内部记录 listener id,注销时查表删除 —— 示例采用此法,简洁且无反射开销
  • 要求用户传入唯一 string 标识符,注销时按标识删 —— 适合跨包注册场景

一次性事件(如初始化完成、资源关闭)可通过在通知后清空对应事件的监听器列表实现,无需额外字段标记。

type EventManager struct { 	mu       sync.RWMutex 	listeners map[string][]func(interface{}) }  func NewEventManager() *EventManager { 	return &EventManager{ 		listeners: make(map[string][]func(interface{})), 	} }  func (e *EventManager) Subscribe(event string, f func(interface{})) func() { 	e.mu.Lock() 	defer e.mu.Unlock()  	e.listeners[event] = append(e.listeners[event], f)  	return func() { 		e.mu.Lock() 		defer e.mu.Unlock() 		if list, ok := e.listeners[event]; ok { 			for i, fn := range list { 				if fn == f { // 注意:仅当 f 是同一函数值时才成立,适用于闭包绑定场景 					e.listeners[event] = append(list[:i], list[i+1:]...) 					break 				} 			} 		} 	} }  func (e *EventManager) Notify(event string, data interface{}) { 	e.mu.RLock() 	list, ok := e.listeners[event] 	e.mu.RUnlock()  	if !ok || len(list) == 0 { 		return 	}  	for _, f := range list { 		go func(fn func(interface{})) { 			defer func() { 				if r := recover(); r != nil { 					// log.Printf("event %s handler panic: %v", event, r) 				} 			}() 			fn(data) 		}(f) 	} }

注意:函数相等性判断(fn == f)在 Go 中仅对函数字面量或同一变量有效;若监听器来自不同闭包(比如带不同捕获变量),需改用标识符方式管理。这是最容易被忽略的兼容性边界。

text=ZqhQzanResources