应手写50行channel+goroutine实现事件总线:带缓冲防阻塞、单goroutine分发防panic中断、context控制超时取消、sync.map管理订阅、event含seqid/timestamp保序可重放。

别用第三方 Event 库做核心事件总线——它大概率掩盖背压、丢消息、难调试,不如手写 50 行 channel + goroutine 更可控。
为什么 event 库在生产环境容易出问题
多数 Go event 库(如 go-eventbus、golang-event)默认用 chan interface{} 或无缓冲 chan Event,导致:
- HTTP handler 调 Publish() 时,若消费者 goroutine 暂时卡住或未启动,直接阻塞整个请求;
- 多个订阅者共用一个 channel,for range ch 分发时一旦某个 EventHandler panic,整个循环退出,后续所有事件静默丢失;
- 回调执行不带 context.Context,无法响应超时或取消,网络请求类回调可能永久挂起;
- 订阅/取消用函数地址做 key(map[func(Event)]bool),热重载后地址失效,Unsubscribe 变成空操作。
用原生 channel 实现可靠异步回调的最小可行结构
不依赖任何库,50 行内完成:带缓冲、防阻塞、可取消、可监控。关键点是把“发送”和“分发”彻底拆开:
-
events chan *Event必须带缓冲(如make(chan *Event, 128)),缓冲大小按峰值 QPS × 平均处理延迟预估,别设 0 或 1024 - 启动唯一分发 goroutine:
go func() { for e := range bus.events { bus.dispatch(e) } }(),避免多个 reader 竞争 - 每个订阅返回
func()取消函数,内部用sync.Map存id → []func(*Event) error,不用map[string][]func防写锁 - 回调签名统一为
func(*Event) error,便于上游记录失败、触发重试或告警
回调执行必须绑定 context 并检查取消
这是最容易被忽略的泄漏点:每次发事件都 go handler(e),但 handler 里有 http.Post 或 time.Sleep,主流程退出后 goroutine 还在跑。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 订阅时传入
ctx,生成带 cancel 的子 context:subCtx, cancel := context.WithTimeout(ctx, 30*time.Second) - 回调执行前加
if subCtx.Err() != nil { return subCtx.Err() } - 用
errgroup.Group启动回调:eg.Go(func() error { return handler(e) }),主流程退出时调eg.Wait()等待全部结束
事件结构体必须含 SeqID 和 Timestamp
纯内存总线不保序也不支持重放,业务一复杂就出错。比如用户注册后立刻发短信,但“用户创建”事件被晚于“短信发送”事件处理。
-
Event结构体加字段:SeqID uint64(服务启动时原子递增)、Timestamp time.Time(构造时time.Now()) - 消费者维护本地
lastSeq uint64,收到事件先比对e.SeqID 则跳过(去重),<code>e.SeqID > lastSeq+1则记录 gap(乱序) - 需要重放时,把历史事件注入临时
chan *Event,再由分发 goroutine 读取——绝不要直接往主bus.events写,否则干扰实时流
真正麻烦的不是写几行 goroutine,而是让每条事件可追溯、可重放、不丢不乱。框架能省代码,但挡不住设计缺陷;手写 channel 不代表原始,而是把控制权拿回来。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










