17370845950

如何在Golang中实现观察者通知机制_Golang观察者模式事件派发示例
Go语言可用map+sync.RWMutex+chan手动实现线程安全观察者模式:用RWMutex保护事件名到回调列表的映射,Notify异步执行并recover panic,Subscribe返回注销函数,注意函数相等性限制。

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 会导致整个通知中断,后续监听器收不到事件。

推荐做法是异步派发:

  • 每个事件通知启动独立 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 中仅对函数字面量或同一变量有效;若监听器来自不同闭包(比如带不同捕获变量),需改用标识符方式管理。这是最容易被忽略的兼容性边界。