go观察者模式需避免死锁、panic和泄漏:chan event须配对goroutine消费,切片遍历前需快照并用rwmutex保护,小事件传值、大事件传指针且确保逃逸,禁止notify内做marshal或加统一context。

Go 里没有 Observer 接口,所谓“观察”本质就是“收到事件后执行函数”,分发方式选错会直接导致 panic、死锁或 goroutine 泄漏。
为什么用 chan Event 分发常出 fatal error: all goroutines are asleep - deadlock
这不是 channel 写错了,而是没配对:带缓冲的 chan Event 要求有独立 goroutine 消费,否则 subject.Notify() 一发就卡住。
- 常见错误:只声明
events := make(chan Event, 1024),但没写go func() { for e := range events { ... } }() - 缓冲区大小不是越大越好:设成 1024 能扛突发,但若消费者长期阻塞(比如 DB 写入超时),缓冲区填满后 sender 仍会阻塞——违背“通知失败不应影响主体逻辑”的原则
- 别让每个 observer 都有自己的 channel:那样得为每个订阅者开 goroutine + channel,内存和调度开销陡增,且注销时容易漏关 channel 导致泄漏
[]func(Event) 同步分发怎么避免遍历时被删 panic
切片本身不线程安全,for _, h := range s.handlers 过程中若另一个 goroutine 调用 Unregister() 修改切片,会触发 fatal error: concurrent map iteration and map write(即使你用的是切片,Go 运行时也统一报这个错)。
- 必须在
Notify()开始前做快照:handlers := append(s.handlers[:0:0], s.handlers...),而不是handlers := s.handlers(后者共享底层数组) - 注册/注销用
sync.RWMutex:写操作(Register/Unregister)调Lock(),读操作(Notify中的快照复制)用RLock() - 注销别边遍历边删:用
map[uintptr]struct{}或唯一 token 标记待移除项,等下次快照时过滤,避免遍历中修改切片引发panic: runtime error: slice bounds out of range
分发时传 *Event 还是 Event 值?
取决于事件结构体大小和 observer 是否异步持有。传错会导致静默崩溃或内存泄漏。
- 小结构体(字段 ≤ 4 个 int/string)直接传值
Event:避免指针解引用开销,GC 无压力 - 大结构体或需 observer 修改内容时传
*Event,但必须确保Event已逃逸到堆上——不能传局部变量地址,例如Notify(&localEvent)是高危操作 - 绝对不要在
Notify()内部做json.Marshal()或构造大对象:那是业务逻辑,不属于分发层;若 observer 需要 JSON,让它自己 marshal
要不要在分发里加 context.Context 控制超时
不该由 Notify() 统一封装,而应交给调用方决定。强行加 context 会让简单场景变重,且无法覆盖所有 observer 的需求差异。
- 同步分发场景:调用方自己用
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond),再传给 handler - 异步分发场景:每个 handler 应自行处理 context,例如
go func(ctx context.Context) { select { case - 别在
Notify()里统一select { case :这会跳过部分 observer,破坏“广播”语义
最易被忽略的点是:observer 执行是否可重入。如果某个 handler 在执行中又调用了 Register(),而你用的是 sync.RWMutex,且在 RLock() 区域内调了它,就会死锁——RWMutex 不支持读锁重入。解决方案只有一个:释放读锁后再注册,或改用 token + sync.Map 方案彻底解耦生命周期。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











