最轻量可控的事件总线应避免裸用 chan interface{},因其导致编译期校验缺失、并发写 map crash、channel 接收循环阻塞等问题;须定义具体 struct 事件类型并用 map[string][]chan 安全管理。

Go 里用 chan 实现事件总线是最轻量且可控的方式,不用引入第三方库也能跑通核心流程,关键是把事件类型、发布/订阅逻辑和生命周期管理收束清楚。
为什么不能裸用 chan interface{} 做全局广播
类型擦除后无法做编译期校验,运行时 panic 风险高;多个 goroutine 并发写入未加锁的 map 会直接 crash;更隐蔽的问题是:一个 handler 处理慢或 panic,会导致整个 channel 接收循环卡住,其他订阅者全被拖垮。
- 事件类型必须定义为具体 struct(如
UserRegisteredEvent),而非字符串或空接口 - 总线内部用
map[string][]chan 存订阅者,读写必须由 <code>sync.RWMutex保护 - 发布时用
select { case ch 非阻塞写,避免单个慢 subscriber 拖累整体 - 每个 subscriber channel 必须带缓冲(如
make(chan Event, 16)),否则一写就阻塞
如何防止 goroutine 和 channel 泄漏
常见错误是订阅者退出时没通知总线,导致总线仍往已关闭或无人接收的 channel 发送事件,最终 goroutine 卡在 select 里出不来,内存持续增长。
- 订阅方法应返回
和 <code>func()取消函数,由调用方控制生命周期 - 取消函数内部要从 map 中删除该 channel,并显式
close(ch) - 不要在 handler goroutine 里
defer close(ch)——context 可能先取消,造成 double-close panic - 测试时用
time.AfterFunc模拟慢 handler,观察runtime.NumGoroutine()是否稳定
怎么让 handler 具备超时和错误隔离能力
handler 若调用外部 HTTP 或 DB,不设超时会卡死整个 channel 接收循环;不捕获 panic 会导致 goroutine 消失且无日志。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
立即学习“go语言免费学习笔记(深入)”;
- 每个 handler 必须运行在独立 goroutine 中,且包裹
recover()+ 日志记录 - 业务逻辑必须接收
context.Context,并在关键调用前检查ctx.Err() - HTTP 调用统一加
context.WithTimeout(ctx, 5*time.Second) - handler 函数签名统一为
func(Event) error,便于后续注入重试、日志等中间件
event name 字符串散落各处会带来什么问题
不同包里硬编码 "user.registered" 这类字符串,会导致拼写错误难发现、重构困难、测试 mock 成本高,上线后事件根本没人订阅。
- 所有事件主题名必须集中定义在
event/types.go,导出为常量(如const UserRegisteredTopic = "user.registered") - 订阅时只允许传入这些常量,禁止字符串字面量
- 总线初始化阶段可加断言:若 topic 不在预定义列表中,直接 panic,避免静默失败
- 测试时可通过
go:generate自动生成 topic 列表文档,同步给前端或运维团队
真正麻烦的不是写一个能跑的 EventBus,而是让所有 handler 都遵守缓冲大小、context 传递、错误返回和取消通知这四条约束——漏一条,线上就可能表现为 goroutine 数缓慢上涨、事件延迟突增或偶发 panic。这些细节不会报错,但会在高并发压测或流量高峰时集中爆发。


















