用 channel 做事件总线是最轻量且可控的方式:利用带缓冲 channel 异步分发、sync.Map 动态管理处理器、每个事件携带 context 控制生命周期,并统一处理 panic 与错误兜底。

用 channel 做事件总线是最轻量且可控的方式
Go 本身没有内置事件系统,但 channel 天然适合做同步/异步事件分发。关键不是“模拟其他语言的 EventBus”,而是利用 chan interface{} 或带类型的 chan *Event 构建松耦合通信。不要用第三方库封装一层抽象来“增强语义”——多数场景反而增加调试难度。
常见错误是直接用无缓冲 channel 导致发布者阻塞,或用有缓冲但容量设错引发丢事件。实际建议:异步通知一律用带缓冲 channel(如 make(chan *Event, 16)),同步响应走函数回调而非阻塞收消息。
- 事件结构体必须导出字段,否则 JSON 序列化或跨包传递会失败
- 避免在 handler 中做耗时操作;需要异步处理时,立刻启
go func()拿走event - 订阅者退出时记得
close(ch)或发退出信号,防止 goroutine 泄漏
用 sync.Map 管理动态注册的事件处理器
硬编码一堆 if event.Type == "user.created" { handleUserCreated(event) } 很难维护。用 sync.Map 存 map[string][]func(*Event) 是更灵活的做法,支持运行时增删 handler,也避免读写竞争。
注意 sync.Map 的 Load 返回 interface{},需类型断言;别直接存裸函数,统一包装为 func(*Event) error,便于错误传播和日志追踪。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 注册时用
m.Store(eventType, append(handlers, fn)),别覆盖原列表 - 触发前先
if v, ok := m.Load(eventType); ok { ... },防止 panic - handler 执行异常不能 panic,必须 recover 并记录日志,否则整个事件循环可能卡死
避免在 HTTP handler 里直接发事件导致超时
典型反模式:http.HandleFunc("/order", func(w r, r *http.Request) { publish(&OrderCreated{...}); w.WriteHeader(200) })。如果下游 handler 耗时,HTTP 响应就被拖慢。
正确做法是:HTTP 层只做验证和数据构造,发事件用非阻塞方式(如带缓冲 channel 或 select + default),或把事件写入本地队列(如 slice)后异步批量提交。
- 不要在
http.Request.Context()里启动长生命周期 goroutine,context 可能提前 cancel - 若依赖外部服务(如 Kafka),发事件失败必须有 fallback:写本地磁盘、重试队列或降级为内存缓存
- 测试时用
time.AfterFunc模拟延迟 handler,验证超时保护是否生效
用 context.Context 控制事件传播生命周期
一个用户注册事件,可能触发发邮件、写搜索索引、更新推荐模型三个子事件。若其中一环超时或取消,其余不该继续执行。这时每个事件实例都该携带 context.Context,并在 handler 开头检查 ctx.Err() != nil。
别把全局 context.Background() 直接传给所有 handler;从原始请求 context 派生带 timeout 的子 context,比如 ctx, _ := context.WithTimeout(parentCtx, 5*time.Second)。
- 事件结构体字段名用
Ctx context.Context,不要叫Context(易与标准库混淆) - 跨 goroutine 传递时,确保 context 不被意外 cancel;必要时用
context.WithValue透传 traceID - 日志打点必须包含
ctx.Value("trace_id"),否则链路追踪断掉


















