事件总线不能直接用channel,因其无法天然广播、难管理多订阅者、易丢事件且panic会崩溃goroutine;应采用带类型过滤、并发执行、panic恢复、安全注册/注销的map+rwmutex方案。

为什么不能直接用 channel 做事件总线
直接用 unbuffered channel 传事件,一发一收,根本不是“派发”,而是同步调用;加了 buffer 又容易丢事件或阻塞 goroutine。更麻烦的是,多个 handler 订阅同一事件类型时,channel 无法天然广播——你得自己做 fan-out,还要处理 panic 恢复、生命周期管理、动态注册/注销。
真正需要的是:事件可广播、handler 可并发执行、失败不中断其他 handler、支持按类型过滤、能安全关闭。
- 别把
chan interface{}当事件总线,它只是管道,不是调度器 - handler 内 panic 会杀掉整个 goroutine,必须用
recover()包一层 - 如果用
map[EventType][]func(Event)存 handler,记得加sync.RWMutex,读多写少场景下RWMutex比Mutex更合适
如何实现带类型过滤与并发执行的 Dispatch 方法
Dispatch 不是往一个 channel 里塞数据,而是遍历匹配该事件类型的全部 handler,并为每个启动独立 goroutine(避免串行阻塞),同时确保 panic 不扩散。
func (e *EventBus) Dispatch(evt Event) {
e.mu.RLock()
handlers := e.handlers[evt.Type()]
e.mu.RUnlock()
var wg sync.WaitGroup
for _, h := range handlers {
wg.Add(1)
go func(handler func(Event)) {
defer wg.Done()
defer func() {
if r := recover(); r != nil {
log.Printf("handler panic: %v", r)
}
}()
handler(evt)
}(h)
}
wg.Wait()
}
- 读取 handler 列表必须在
RUnlock()后立刻完成,否则可能被并发Subscribe修改 map - 不要在 goroutine 闭包里直接引用循环变量
h,必须显式传参,否则所有 goroutine 共享最后一个h - 这里用
wg.Wait()是为了方便测试和控制生命周期;生产环境若允许 fire-and-forget,可去掉wg,但需额外考虑背压或日志丢失问题
Subscribe 和 Unsubscribe 怎么做到线程安全且不漏事件
订阅和退订操作本身要快,且不能阻塞 Dispatch。典型错误是:在 Subscribe 里加 Mutex 锁住整个 map,然后复制 slice —— 这会让高频 Dispatch 频繁等待写锁。
- 推荐用
sync.Map存map[EventType]*sync.Map,但注意sync.Map不适合存 slice,所以外层用RWMutex+ 普通 map,内层 handler 列表用 slice + 读锁保护,写操作只在Subscribe/Unsubscribe时发生,频率低,可接受短时间写锁 -
Unsubscribe不能简单用==比较函数值(Go 中函数不可比较),应要求 caller 传入唯一id string,或用funcptr(unsafe.Pointer(reflect.ValueOf(h).Pointer()))做 key,但后者不稳定,建议业务层自己管 id - 如果 handler 是闭包或方法值,每次调用生成新函数实例,
Unsubscribe前必须保留原始 handler 引用,否则删不掉
要不要支持中间件和异步队列缓冲
纯内存事件总线够用时,加中间件(如日志、校验、重试)反而增加延迟和复杂度。但当 handler 执行慢、或需跨进程/跨机器时,就得引入缓冲队列(如 buffered channel 或 ring buffer)甚至外部消息系统。
- 内存内加缓冲:用
chan Event做输入队列,后台 goroutine 消费并Dispatch,能削峰但不解决 handler 失败重试问题 - 中间件链建议用函数组合:
type Middleware func(Handler) Handler,在Subscribe时包装 handler,而不是在Dispatch里统一注入——后者会让所有 handler 被强制套同一层逻辑 - 真正难的是“事件投递成功”的定义:是写入 channel 就算成功?还是等所有 handler 执行完?前者快但不可靠,后者慢但可控;选哪个取决于业务对一致性和实时性的权衡
最易被忽略的点:handler 注册顺序不影响执行顺序,但多个 handler 修改同一份事件数据时,竞态就藏不住了——事件对象要么设计成不可变,要么让 handler 拿到副本。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











