用chan实现事件总线易丢消息,因无缓冲channel发送会阻塞直至被接收;若订阅者处理慢、未启动或崩溃,发布端将卡死或panic。缓冲channel仅缓解问题,缓冲区大小难预估——过小仍丢消息,过大浪费内存并掩盖设计缺陷。

为什么用 chan 实现事件总线容易丢消息?
因为 Go 的 channel 默认是无缓冲的,send 会阻塞直到有 goroutine 在另一端 receive;一旦订阅者处理慢、卡住或还没启动,发布端就卡死或 panic。缓冲 channel 能缓解,但缓冲区大小难预估——设小了照样丢,设大了吃内存、掩盖设计问题。
- 别用
make(chan Event)直接当事件总线,这是最常见误用 - 如果必须用 channel,至少用带缓冲的:
make(chan Event, 1024),但要配套超时或非阻塞发送 - 更稳妥的做法:把 channel 当“投递通道”,后端用 goroutine 拉取 + 分发到各订阅者,避免发布端被拖住
如何让多个订阅者同时收到同一事件?
原生 channel 是点对点的,一个 recv 只能消费一次。要广播,得在事件分发层做复制——不能靠多个 goroutine 同时从一个 channel 读(会抢消息)。
- 维护一个
map[subscriberID]chan,每次发布时遍历 map 发送 - 每个 subscriber 自己开 goroutine 从专属 channel 读,避免互相阻塞
- 注意:订阅/退订操作需加锁(
sync.RWMutex),否则并发写 map panic - 示例关键逻辑:
for _, ch := range bus.subs { select { case ch <- evt: default: // 丢弃或打日志,不阻塞主流程 } }
select 配 default 会导致事件积压吗?
会,而且很隐蔽。用 select { case ch 看似防阻塞,但如果 subscriber 处理太慢,它的 channel 缓冲区满后,后续所有事件都会进 <code>default 被丢弃,且毫无提示。
- 丢弃前至少记一条 warn 日志:
log.Warn("event dropped, subscriber busy", "id", id) - 更合理的是用
select+ 超时:case ch - 长期积压说明架构失衡——考虑限流(如令牌桶)、降级(只传关键字段)或换用消息队列
goroutine 泄漏比 channel 死锁更难发现
每个 subscriber 开一个 for range ch goroutine 很常见,但没人通知它退出。退订时只关 channel 不停 goroutine,就会永久等待,累积成泄漏。
立即学习“go语言免费学习笔记(深入)”;
- 关 channel 前,先发一个 sentinel 事件(如
Event{Type: "unsubscribe"}),让 goroutine 主动 return - 或者用
context.Context控制生命周期:for { select { case - 用
pprof定期查/debug/pprof/goroutine?debug=1,看数量是否随时间增长
实际跑起来,最难调的不是怎么发事件,而是谁在什么时候、以什么节奏收事件——channel 只是管道,堵在哪、漏在哪,得靠上下文里的超时、日志、监控一起定位。










