因为默认的 channel 是同步的:发布者往 ch 中发送消息时,若无协程立即接收,就会阻塞等待。

Go 里用 channel 实现观察者,为什么通知会阻塞?
因为默认的 channel 是同步的:发布者往 ch 写入时,必须等某个观察者从 channel 读走这个值,才会继续往下走。这在多数业务场景下反而是隐患——一个慢观察者(比如写日志到磁盘、调远程 HTTP)会拖垮整个事件流。
- 别用无缓冲
chan Event做通知通道,除非你明确要串行、强顺序、且所有观察者都极轻量 - 缓冲 channel(如
make(chan Event, 100))能缓解阻塞,但只是把问题从“立刻卡住”变成“缓存满后卡住”,没根治 - 真正解耦得靠 goroutine:发布者不直接写 channel,而是起一个
go func()异步转发
如何安全地异步通知多个观察者?
核心是避免观察者 panic 或死锁影响其他观察者。不能让一个观察者的崩溃导致整个通知链中断,也不能让多个观察者并发读同一个 channel 时互相干扰。
- 每个观察者应独占自己的接收
chan Event,而不是共享一个 channel - 注册时保存的是
func(Event)回调函数,而非 channel;通知时用go obs(event)启动独立 goroutine - 务必加 recover:用
defer func() { _ = recover() }()包住每个回调执行,否则一个 panic 会让整个 goroutine 消失,还可能泄露 - 如果观察者需要返回结果(比如校验是否允许操作),那就不能异步——得回归同步模型,或改用带响应 channel 的 request-reply 模式
sync.Map 存观察者列表,真的比 map + mutex 快吗?
不一定。只有在读多写少、且写操作分散在大量 goroutine 中时,sync.Map 才有优势。观察者模式中,注册/注销(写)通常远少于通知(读),而且注册行为本身往往集中在初始化或配置变更阶段。
- 日常使用
map[string]func(Event)+sync.RWMutex更清晰、更易测试、内存更紧凑 -
sync.Map的零值不是空 map,首次写入才初始化内部结构,容易在单元测试里漏掉边界 case - 它不支持遍历,想“广播给所有观察者”就得额外维护一份 key 列表,反而增加复杂度
- 实测在 100 个以内观察者时,
sync.RWMutex读锁开销几乎可忽略,别过早优化
通知顺序能保证吗?同步 vs 异步下的差异
同步通知天然保序:A 注册早 → A 先收到;异步通知则完全不保证。goroutine 启动时机、调度延迟、甚至 GC STW 都会影响实际执行顺序。
立即学习“go语言免费学习笔记(深入)”;
- 如果你依赖通知顺序(比如先校验再审计),就别用异步;要么强制同步,要么在事件里加
seq uint64字段,由观察者自己排序处理 - 异步下即使用
for range遍历 map 并依次go f(e),也不等于按 map 遍历顺序执行——goroutine 调度不承诺 FIFO - 跨 goroutine 的顺序需求,本质是状态协同问题,该上消息队列或状态机的地方,硬靠观察者模式扛不住
顺序和性能往往互斥。选哪个,取决于你的事件是不是“可重入”“可乱序”“可丢失”。这些约束比怎么写 channel 更关键,但最容易被跳过。











