用 sync.map 实现线程安全事件注册表,适合读多写少场景;注册用 store,触发前 load 后遍历调用 handler,禁止遍历中 delete;需动态注销应改用带锁自定义结构;默认同步执行 handler,异步需用 worker pool 控制并发。

用 sync.Map 实现线程安全的事件注册表
Go 原生没有内置的 Observer 接口,直接用 map 存订阅者会 panic:「concurrent map read and map write」。必须自己加锁或选并发安全结构。sync.Map 是最轻量的选择,适合读多写少的事件场景(比如配置变更、状态通知),但注意它不支持遍历中删除——触发事件时不能边遍历边取消订阅。
- 注册用
Store(key, value),key 通常用string类型的事件名,value 是[]func(interface{})切片 - 触发前用
Load(key)拿到 handler 列表,再遍历调用;别在遍历中调Delete() - 如果需要支持动态注销,改用带互斥锁的自定义结构,而不是强塞
sync.Map
避免 goroutine 泄漏:事件回调里别直接起 go func()
常见错误是每个事件都开 goroutine 异步执行 handler,结果 handler 执行慢或阻塞,导致 goroutine 积压。尤其在高频事件(如日志推送、心跳)下,几秒就上千个 goroutine。
- 默认同步执行 handler,让调用方决定是否异步——更可控
- 真要异步,统一用 worker pool 控制并发数,比如用
chan func()+ 固定数量的 goroutine 消费 - handler 内部若含网络调用,务必设超时,否则整个事件分发链会被拖住
interface{} 传参太松散?用具体事件类型替代
很多示例用 func(event interface{}) 当 handler 签名,看似灵活,实际导致类型断言泛滥、运行时报错难定位。比如发的是 *UserCreatedEvent,但某个 handler 错写成 event.(*OrderEvent),一触发就 panic。
- 为每类事件定义专用 handler 类型:
type UserEventHandler func(*UserCreatedEvent) - 注册表按事件类型分桶:
map[string][]UserEventHandler,而不是全塞进一个大 map - 发布时强制类型匹配,编译期就能发现 handler 和事件不搭
测试事件流时,别 mock 全局状态,用 chan 捕获触发信号
单元测试里验证「某事件发出后,A 和 B handler 是否被调用」,如果依赖真实注册表,容易因测试顺序污染状态。更干净的做法是把 handler 改成向 channel 发信号,测试只收 channel 数据。
立即学习“go语言免费学习笔记(深入)”;
- 写测试用的临时 handler:
done := make(chan struct{}, 2); handler := func(e *UserCreatedEvent) { done - 注册后触发事件,用
select { case 验证 - 这样不碰全局 map,也不依赖 sleep,稳定且快
事情说清了就结束。真正麻烦的不是注册和触发,而是 handler 里混了数据库操作、HTTP 调用、又没 context 控制,一出问题整条事件链就卡死——这点比模式本身更值得盯紧。










