Go进程内pub/sub事件总线:快Channel4倍


Go应用程序的简单内部事件总线, 高性能:每秒处理数百万个事件,比Go原来的普通Channel快 4 倍到 10 倍。

快速的进程内事件调度程序

这个包为Go提供了一个高性能的进程内事件调度器,非常适合解耦模块和启用异步事件处理。它支持同步和异步处理,专注于速度和简单性。

  • 高性能:每秒处理数百万个事件,比通道快4倍到10倍。
  • 泛型:适用于实现Event接口的任何类型。
  • Asynchronous:每个订阅者在自己的goroutine中运行,确保非阻塞事件处理。

使用条件:

  • 在单个Go进程中解耦模块。
  • 实现轻量级发布/订阅或事件驱动模式。
  • 需要高吞吐量、低延迟的事件调度。
  • 更喜欢简单、无依赖性的解决方案。

不适用于:

  • 进程/服务间通信(使用Kafka、NATS等)。
  • 事件持久性、耐久性或高级路由/过滤。
  • 跨语言/平台场景。
  • 事件重播、死信队列或重复数据删除。
  • 大量订阅/退订用户流失或大量动态订阅用户计数。

通用进程内发布/订阅
这个存储库包含一个简单的进程内事件调度器,用于解耦内部模块。它提供了一种通用的方式来定义事件、发布和订阅事件。

// Various event types
const EventA = 0x01

// Event type for testing purposes
type myEvent struct{
    Data string
}

// Type returns the event type
func (ev myEvent) Type() uint32 {
    return EventA
}

使用默认调度程序
为了方便起见,该包提供了一个默认的全局调度程序,可与On()和Emit()包级函数一起使用。

// Subcribe to event A, and automatically unsubscribe at the end
defer event.On(func(e Event) {
    println("(consumer)", e.Data)
})()

// Publish few events
event.Emit(newEventA("event 1"))
event.Emit(newEventA("event 2"))
event.Emit(newEventA("event 3"))

网友辣评1:
简单瞅了一眼,这个设计就像个"大喇叭广播站",特别适合搞大规模群发消息。不过Go原来的channel能干的事儿可比这多多了。

它的工作方式是这样的:

  1. 喜欢听同一类消息的小伙伴们会被分到一个"追星群"里
  2. 整个群就一把钥匙(锁),谁要发消息得先抢到这把钥匙
  3. 拿到钥匙后,发消息的人可以把消息一次性复制到所有粉丝的收件箱
  4. 粉丝们收消息时也是抢钥匙,但一抢到就能把整个收件箱(最多128条消息)都倒进自己包里
对比起来:
  • Go原来的channel 就像小卖部,每次买卖都要开关一次收银机(锁)
  • 这个设计像批发市场,一次交易能搬走好几箱货
还有个要注意的:这系统有个"班长"会不停点名检查谁在群里,就算没人发消息它也会隔三差五喊"到!"。所以要是消息很少的时候,它就像个睡不着觉的夜猫子,CPU都没法好好休息。

网友辣评2:
如果您需要在一个进程内进行非常轻量的发布/订阅,这可能对某些人有用。

我正在用 Go 语言写个小破多人游戏。最开始用的是 Go 自带的‘传话筒’(通道Channel扇出),但纯粹是手痒想试试能不能搞得更猛一点。于是自己搓了个‘光速快递站’(微型事件总线)来测试。

结果在我的 i7-13700K 顶级 CPU 上(对,就是那种开机比眨眼还快的怪物),传个消息只要 10-40 纳秒 —— 啥概念?比你用普通‘传话筒’快 4-10 倍!具体多快,得看你怎么调教它。

网友辣评3:
比Go原来的普通Channel快 4 倍到 10 倍?
通道(channel)就像 Go 语言里的共享内存快递站!」

  • 当你有一堆 Goroutine(可以理解成一群打工仔)要互相传数据时,通道是最稳的选择。
  • 它自带「防打架机制」(互斥锁),但不用你手动加锁,Go 官方已经帮你调教好了,稳得很。
  • 所有用通道的人都共享同一套安全机制,不会出现「你锁你的,我锁我的,最后死锁了」的尴尬局面。

「别被它的外表骗了!通道Channel ≠ 万能队列!」

  • 通道Channel 能缓冲、能阻塞,确实像个队列,但……
  • 很多新手一看到通道Channel 就狂喜:「这不就是现成的队列吗?拿来吧你!」 结果用着用着发现不对劲——它其实没那么适合当普通队列用。
  • 就像你拿「外卖保温箱」当「衣柜」用,短期能凑合,但时间久了会发现——「这玩意儿塞衣服怎么这么别扭?」

一旦你链接了多个通道,通道就会变得非常难以调试。忘记关闭通道,整个管道就会停滞。通道也很慢,即使在数据不需要锁定并且可以直接在函数之间传递的情况下,也需要互斥锁定。

「通道用多了,debug 时会哭的!」

  1. 「背压问题」(数据堵车):
    • 通道默认不带「智能交通管制」,数据一多就可能堵死。
    • 你得自己搞「缓冲带」(中间缓冲区),否则就像早高峰的地铁站,挤爆了!
  • 「忘记关通道?完蛋!」
    • 就像你忘关水龙头,整个水管(管道)都会卡住,Goroutine 全变僵尸,程序直接躺平。
  • 「通道其实挺慢的!」
    • 哪怕数据根本不需要加锁(比如纯函数传参),通道还是会傻乎乎地走锁机制,「脱裤子放屁——多此一举」。

    很多库(例如 Rill 和 go-stream)都涌现出来,它们包装通道Channel来建模数据管道Stream(尤其是有了泛型,构建诸如重复数据删除、扇出、缓冲等通用操作符变得更加容易),但我发现这样做并不明智。通道应该仍然是构建管道的底层原语,但不应该将其用作主要的 API 接口。

    「别拿通道Channel当 API 耍帅,会翻车的!」

    • 现在有些库(比如 Rill、go-stream)把通道Channel包装成「高级数据流水线」,用泛型搞什么去重、广播、缓冲……
    • 但!这其实是个坑! 通道适合当「底层螺丝钉」,但不适合直接当「外包装」。
    • 就像你不能拿「钢筋水泥」直接当「精装修房」卖,得先砌墙、刷漆、装家具啊!


    ✅ 通道适合: Goroutine 之间安全传数据,自带防死锁机制。
    ❌ 通道不适合:
    当普通队列用(它真不是干这个的!)
    复杂数据流水线(debug 会疯的!)
    高性能场景(锁来锁去,慢得很!)
    「通道就像 Go 语言里的交通警察,管 Goroutine 传数据很稳,但别让它去干交警不该干的活儿!」

    网友辣评4:
    不知道它和LMAX Disruptor 的设计有没有共通之处
    最近,我在许多需要线程间同步原语的 .NET 实现中,从 Disruptor.NET 切换到了 Channel。Disruptor 的速度可能更快,但我非常喜欢内置类型的语义。
    https://learn.microsoft.com/en-us/dotnet/core/extensions/channels
    https://learn.microsoft.com/en-us/dotnet/api/system.threading.channels.channel-1?view=net-9.0