不能直接用chan interface{}做总线,因其导致类型擦除无法编译检查、并发写map panic、单个handler阻塞或panic拖垮全部订阅者;须定义具体事件struct,用map[string][]chan Event配合sync.RWMutex保护,并为每个channel设置缓冲。

为什么不能直接用 chan interface{} 做总线
裸用 chan interface{} 看似简单,实际会埋三个雷:类型擦除导致编译期无法检查事件结构是否匹配,多个 goroutine 并发写 map 时没加锁直接 panic,更隐蔽的是——一个 handler 处理慢或 panic,整个 range 循环就卡死,其他订阅者全被拖垮。
必须定义具体事件 struct(比如 UserRegisteredEvent),总线内部用 map[string][]chan Event 存订阅者,并配 sync.RWMutex 保护读写。
- 事件类型别名或常量统一定义在
event/types.go,例如const UserRegisteredTopic = "user.registered",避免字符串散落各处拼错 - 每个
chan Event必须带缓冲,比如make(chan Event, 16),否则 publish 时无人接收会阻塞甚至 panic - 发布逻辑必须用
select { case ch 非阻塞写,跳过已满或已关闭的 channel
如何安全启动和注销订阅者 goroutine
真正的难点不是“怎么注册”,而是“怎么让 goroutine 自己干净退出”。常见错误是启动了 goroutine 监听 channel,但没人通知它停,结果内存泄漏、goroutine 数持续上涨。
订阅方法应返回一个取消函数:func() error,由调用方控制生命周期。这个函数要干三件事:从 map 中删掉该 channel、显式 close(ch)、通知监听 goroutine 退出。
立即学习“go语言免费学习笔记(深入)”;
- 监听 goroutine 内部用
select { case e := 检测注销信号 - 别在 handler goroutine 里
defer close(ch)—— context 可能先取消,造成 double-close panic - 测试时可用
time.AfterFunc模拟慢 handler,观察runtime.NumGoroutine()是否回落
handler 怎么做到错误隔离和超时控制
一个 handler 调外部 HTTP 或 DB 不设超时,会卡死整个事件流;不 recover panic,goroutine 就静默消失,连日志都没有。
每个 handler 必须运行在独立 goroutine 中,并包裹 recover() + 日志记录。业务逻辑签名建议统一为 func(Event) error,便于后续插中间件。
- handler 函数内第一行就检查
ctx.Err(),所有关键调用都套context.WithTimeout(ctx, 5*time.Second) - HTTP 客户端、DB 查询等必须传入该 ctx,而不是用全局或 background ctx
- publish 方法签名建议是
Publish(ctx context.Context, event Event) error,失败时可记录或转发告警
什么时候该放弃手写总线,换 Kafka 或 Redis
Go 的 channel 只适合进程内通信。一旦出现以下任一情况,手写总线很快失控:
- 需要跨进程/跨机器分发事件(比如微服务间)
- 要求事件持久化、重放、按 offset 手动提交
- 要支持过滤器(如按 topic/subtopic 匹配)、中间件(日志、重试、熔断)
- 事件量大到单机 channel 缓冲撑不住,或需高可用保障
这时直接上 github.com/segmentio/kafka-go 或 github.com/go-redis/redis/v9 更可靠。自己写的 bus 最大价值是清晰、可控、零依赖,但边界很明确:它不是消息队列替代品,只是解耦工具。



















