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

Go 语言没有内置的观察者模式支持,也没有类似 EventEmitter 的标准库类型,所以必须手动实现——但不需要第三方库,用 map + sync.RWMutex + chan 就能写出线程安全、低开销、可取消的通知机制。
如何用 map 和 sync.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,不传播到其他监听器 - 不加
waitgroup或chan等待回调结束——发布者只负责“发出”,不关心“送达”
示例中 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 中仅对函数字面量或同一变量有效;若监听器来自不同闭包(比如带不同捕获变量),需改用标识符方式管理。这是最容易被忽略的兼容性边界。










