裸用chan interface{}必然出问题,因其导致类型不安全、并发写map崩溃、单点阻塞拖垮全局;应封装map[string][]chan *Event,配sync.RWMutex保护、带缓冲typed channel及非阻塞select分发。

Go 语言没有内置事件总线,但用 channel + select + 结构体就能跑通 90% 的进程内事件场景;关键不是“模拟其他语言”,而是守住缓冲、类型、生命周期这三条底线。
为什么裸用 chan interface{} 必然出问题
很多人一上来就写 events := make(chan interface{}),然后往里塞各种结构体——这会立刻踩中三个坑:
- 类型擦除后无法做编译期校验,
type switch写错或漏分支,运行时 panic 不报来源 - 多个 goroutine 并发写入未加锁的 map(比如按 type 分发时)直接触发
fatal error: concurrent map writes - 一个 handler 处理慢或 panic,整个
for range events循环就卡死或退出,其余订阅者全被拖垮
怎么用 map[string][]chan *Event 安全分发
真正可控的做法是把 topic 当 key,每个 topic 对应一组带缓冲的 typed channel:
- 定义具体事件结构体,如
type OrderCreatedEvent struct { OrderID int64; UserID int64 },字段全导出、值类型优先 - 总线内部用
sync.RWMutex保护subscribers map[string][]chan *Event,注册/注销必须加锁 - 发布时遍历对应 topic 的所有
chan,每条都用select { case ch 防阻塞 - 每个
chan必须带缓冲,make(chan *Event, 16)是较稳妥起点;别设 0(易阻塞)也别设 1024(内存堆积)
如何防止 goroutine 和 channel 泄漏
泄漏不是“没关 channel”那么简单,而是“没人通知它该停了”。常见错误包括:
立即学习“go语言免费学习笔记(深入)”;
- handler 启动的 goroutine 持有
chan引用,但总线注销时只从 map 删除 key,没 close 对应 channel - 在 HTTP handler 里直接
go handle(event),却没传context.Context,请求结束、context cancel 后 goroutine 还在跑 - 用
time.Ticker触发事件,但没调用ticker.Stop(),它不会随 context 自动停止 - 订阅方法必须返回
func()取消函数,且内部要:① 从 map 删除 channel、②close(ch)、③ 清理关联资源(如 http.Client)
什么时候该放弃自己写,换 Kafka / Redis
当出现以下任一信号,说明你已超出 channel 的能力边界:
- 需要保证事件顺序(
channel无法跨 goroutine 保序,尤其多消费者时) - 要求失败重试、死信队列、消费确认(ACK),而你不想在内存里手写重试逻辑和持久化队列)
- 事件要跨进程、跨机器分发(
channel是纯内存的,连 goroutine 都跨不了) - 已有 Kafka 或 Redis,且团队熟悉其运维——直接用
github.com/segmentio/kafka-go或github.com/go-redis/redis/v9更可靠
真正难的不是让事件“发出去”,而是想清楚:这条事件丢了行不行?重不重试?上下游是不是不同语言?这些判断比写个 publish() 函数重要得多。



















