Go观察者模式需手动实现,核心是用sync.RWMutex+slice安全管理订阅者,通知时复制列表并goroutine并发调用,接口应轻量明确,生命周期管理防泄漏。

Go 语言没有内置的事件系统或接口继承机制,观察者模式必须手动实现;核心在于用 map 或 slice 管理订阅者,并确保通知时避免并发写 panic 和重复注册/移除问题。
用 sync.Map 存储观察者避免竞态
多个 goroutine 同时调用 Register / Unregister 时,普通 map 会 panic。虽然 sync.Map 不支持遍历,但可配合额外的 sync.RWMutex + slice 实现安全读写:
- 只在注册/注销时加写锁,通知时用读锁遍历副本,性能更可控
- 不要直接在通知循环中调用观察者方法——若某个观察者阻塞或 panic,会拖垮整个通知流
- 推荐为每个观察者启动独立 goroutine(加
recover),但需注意资源泄漏风险
Observer 接口定义要轻量且明确
Go 中接口越小越好。观察者只需一个方法,参数应包含事件类型和有效载荷,不建议传入发布者实例(破坏解耦):
type Event string
const (
EventUserCreated Event = "user_created"
EventOrderPaid Event = "order_paid"
)
type Observer interface {
OnEvent(event Event, data interface{})
}
-
data interface{}提供灵活性,但调用方需保证类型一致,否则运行时报错 - 避免定义
Update()这类模糊方法名,无法体现事件语义 - 如需区分事件子类型,可用结构体嵌套
Event字段,而非靠接口方法重载
通知时复制观察者列表防止修改冲突
不能边遍历边修改切片,否则可能漏通知或 panic。典型做法是:读锁保护下拷贝当前列表,释放锁后再逐个通知:
立即学习“go语言免费学习笔记(深入)”;
func (e *EventBus) Notify(event Event, data interface{}) {
e.mu.RLock()
observers := make([]Observer, len(e.observers))
copy(observers, e.observers)
e.mu.RUnlock()
for _, obs := range observers {
go func(o Observer) {
defer func() {
if r := recover(); r != nil {
log.Printf("observer panic: %v", r)
}
}()
o.OnEvent(event, data)
}(obs)
}
}
-
copy()前必须已加读锁,否则可能拷贝到部分更新状态 - 闭包中传参用
go func(o Observer)而非直接obs,避免循环变量复用问题 - 如果业务要求强顺序或同步响应,去掉
go并移除recover,但需接受单点失败影响全局
真正难的不是注册和通知逻辑,而是生命周期管理:观察者何时该被自动清理?比如 HTTP handler 注册了观察者,但 handler 返回后没人调用 Unregister,就会导致内存泄露。这种场景下,考虑用 context.Context 关联取消信号,或让观察者实现 io.Closer 接口显式退出。










