Go中Observer模式需手动定义Observer和Subject接口,用sync.RWMutex保护观察者列表,Notify时应复制切片再遍历,避免并发问题;事件粒度要适中,状态同步需Observer显式处理,高并发场景可改用channel优化。

Observer 模式在 Go 中没有内置接口,得自己定义 Observer 和 Subject
Go 不像 Java 或 C# 那样有语言级的观察者抽象,所以不能直接 import 一个 Observer 接口。你必须手动定义两个核心契约:Observer 接口(含 Update 方法)和 Subject 接口(含 Attach、Detach、Notify)。否则容易写成“伪观察者”——比如用闭包临时传回调,但无法动态增删监听器,也不利于测试。
典型错误是把状态变更逻辑全塞进 Notify,结果每次通知都触发完整重算,性能崩了。正确做法是让 Update 只接收必要参数(如变更字段名 + 新值),由 Observer 自行决定是否响应、如何更新局部状态。
示例最小契约:
type Observer interface {
Update(event string, data interface{})
}
type Subject interface {
Attach(obs Observer)
Detach(obs Observer)
Notify(event string, data interface{})
}
用 sync.RWMutex 保护观察者列表,避免并发 panic
多个 goroutine 同时调用 Attach/Detach 或 Notify 时,若底层用 []Observer 存储监听器,会触发 “concurrent map iteration and map write” 类似错误(即使没用 map,切片底层数组扩容也可能被并发读写破坏)。
立即学习“go语言免费学习笔记(深入)”;
必须加锁,且推荐 sync.RWMutex:读多写少场景下,Notify 可并发执行(只读切片),而 Attach/Detach 用写锁。
- 别用
sync.Mutex—— 会阻塞所有Notify调用,变成串行通知 - 别在
Notify里直接遍历并调用obs.Update()时不复制切片 —— 若某个Update内部又调用了Detach,会导致切片长度突变,引发 panic - 安全做法:在
Notify开头先lock.RLock(),复制当前观察者切片,unlock,再遍历副本调用Update
状态同步不是自动发生的,Observer 必须显式订阅 + 显式处理变更
Go 的观察者模式不提供“响应式数据绑定”。比如你改了 subject.state.Name,不会自动触发 UI 更新。Observer 必须:
- 在
Update中判断event == "name_changed"才去刷新 name 字段 - 自己维护一份缓存状态(如 struct 字段或 map),避免反复查主状态源
- 注意事件粒度:发太粗(如
"state_updated")导致无效重绘;发太细(如"name_char_at_3_changed")增加耦合
常见陷阱是让 Observer 直接持有 *Subject 并在 Update 里调 GetState() —— 这本质是轮询,失去了事件驱动的意义,还可能因锁竞争卡住。
用 channel 替代切片存储 Observer 更适合高并发、低延迟场景
当观察者数量大(>100)、通知频率高(如每毫秒多次)、且对延迟敏感(如实时音视频状态同步)时,切片 + 锁模型会成为瓶颈。此时可改用无缓冲 channel:
- 每个 Observer 启一个 goroutine 监听专属
chan Event -
Subject.Notify改为向所有 observer 的 channel 发送Event结构体 - channel 天然线程安全,免锁;goroutine 可做背压(如用带缓冲 channel 控制积压量)
代价是内存开销略高(每个 Observer 多一个 goroutine + channel),且需额外管理生命周期(关闭 channel 防 goroutine 泄漏)。简单应用别上这个,但做 IM 状态同步、游戏客户端帧同步时值得考虑。
真正难的从来不是怎么写 notify,而是怎么让 Observer 在收到 event 后,精准、高效、无副作用地同步那一小块状态——这需要你对业务数据流有足够理解,而不是套个模式就完事。










