go需自行实现观察者模式,核心是带sync.rwmutex保护的eventbus(map[string][]func(interface{})),事件名用string、回调参数为interface{},注册需去重(推荐uintptr(unsafe.pointer(&fn))),支持同步/异步通知及defer式注销。

Go里没有内置Observer接口,得自己搭结构
Go语言不提供像Java那样的Observer抽象类或C#的event关键字,观察者模式必须靠组合+函数类型+并发安全容器来实现。核心是定义一个事件分发器(EventBus),内部维护map[string][]func(interface{}),键为事件名,值为回调切片。
常见错误是直接用map并发读写——没加锁会导致panic: fatal error: concurrent map read and map write。必须用sync.RWMutex或sync.Map(后者适合读多写少,但不支持遍历全部监听器)。
实操建议:
- 用
sync.RWMutex保护注册/注销/通知全过程,比sync.Map更可控 - 事件名用
string而非常量枚举,方便动态扩展,但需文档约定命名规范(如"user.created") - 回调函数参数统一为
interface{},由观察者自行断言,避免泛型在早期Go版本中带来的复杂度
注册和注销观察者要支持重复注册防护
多个相同回调反复Subscribe("order.paid", handler)会导致同一事件触发多次执行,这是高频bug。不能只靠用户自觉,得在Subscribe里做去重。
立即学习“go语言免费学习笔记(深入)”;
典型做法是把回调包装成带唯一ID的结构体:type observer struct { id string; fn func(interface{}) },注册时检查id是否已存在;或者用func指针地址做key(仅限闭包外的具名函数,匿名函数地址不可靠)。
实操建议:
- 推荐用
uintptr(unsafe.Pointer(&fn))获取函数地址做去重依据,适用于所有函数值(包括闭包) -
Unsubscribe必须支持按事件名+回调双条件删除,否则无法精准清理 - 注册返回一个
unsubscribe()函数,方便defer调用,比手动Unsubscribe更不易遗漏
通知过程要区分同步 vs 异步,别卡住主流程
默认同步通知(逐个调用回调)看似简单,但某个观察者panic或阻塞会拖垮整个事件流。比如支付成功后发短信、写日志、更新缓存三个观察者,其中一个HTTP超时,其余两个就等死。
异步通知用go e.notifyAsync(event, data)能解耦,但引入新问题:观察者执行顺序不确定、错误无法回传、生命周期难管理。
实操建议:
- 默认走同步,仅对明确标记
async:true的事件才启用goroutine - 异步通知中用
recover()捕获panic,避免goroutine泄露 - 不要在通知里传指针给观察者——原对象可能已被回收,应传深拷贝或只传不可变数据(如
struct{ID int; Time time.Time})
用channel实现轻量级事件总线更贴近Go惯用法
比起维护map+锁,用chan Event做中心队列更符合Go“通过通信共享内存”的哲学。每个观察者起一个goroutine从channel收事件,自行过滤关心的类型。
缺点是无法动态增删监听器(channel关闭后新加的收不到),且广播需遍历所有监听channel,效率不如map查找。但它天然规避了并发写map问题,也更容易做背压控制(如带缓冲的channel)。
实操建议:
- 用
select配合default防止阻塞:select { case ch - 监听器goroutine退出前务必
close(ch)并从全局监听列表移除,否则内存泄漏 - 如果需要事件确认机制(如Kafka的ack),就得在channel消息里附带
done chan struct{},由观察者通知完成
真正麻烦的是跨服务事件(比如订单服务发事件,库存服务监听),这时候Go原生机制就不够用了——得上消息队列。本地观察者模式再怎么优化,也只是单进程内的协作契约。










