0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Golang事件总线机制的实现

Linux爱好者 来源:wangbjun.site 作者:wangbjun.site 2022-07-01 16:02 次阅读

【导读】本文介绍了事件总线实现。

最近在学习开源项目Grafana的代码,发现作者实现了一个事件总线的机制,在项目里面大量应用,效果也非常好,代码也比较简单,介绍给大家看看。

源码文件地址:grafana/bus.go at main · grafana/grafana · GitHub

1.注册和调用

在这个项目里面随处可见这种写法:

funcValidateOrgAlert(c*models.ReqContext){
id:=c.ParamsInt64(":alertId")

query:=models.GetAlertByIdQuery{Id:id}

iferr:=bus.Dispatch(&query);err!=nil{
c.JsonApiErr(404,"Alertnotfound",nil)
return
}

ifc.OrgId!=query.Result.OrgId{
c.JsonApiErr(403,"Youarenotallowedtoedit/viewalert",nil)
return
}
}

关键是bus.Dispatch(&query)这段代码,它的参数是一个结构体GetAlertByIdQuery,内容如下:

typeGetAlertByIdQuerystruct{
Idint64
Result*Alert
}

根据名字可以看出这个方法就是通过Id去查询Alert,其中Alert结构体就是结果对象,这里就不贴出来了。

通过查看源码可以得知,Dispatch背后是调用了GetAlertById这个方法,然后把结果赋值到query参数的Result中返回。

funcGetAlertById(query*models.GetAlertByIdQuery)error{
alert:=models.Alert{}
has,err:=x.ID(query.Id).Get(&alert)
if!has{
returnfmt.Errorf("couldnotfindalert")
}
iferr!=nil{
returnerr
}
query.Result=&alert
returnnil
}

问题来了,这是怎么实现的呢?Dispatch到底做了哪些操作?这样做有什么好处?

下面我来一一解答:

首先,在Dispatch之前,你需要先注册这个方法,也就是调用AddHandler,在这个项目里面可以看到init函数里面有大量这样的代码:

funcinit(){
bus.AddHandler("sql",SaveAlerts)
bus.AddHandler("sql",HandleAlertsQuery)
bus.AddHandler("sql",GetAlertById)
...
}

其实这个方法的逻辑也很简单,所谓注册也就是把通过一个map把函数名和对应的函数做一个映射关系保存起来,当我们Dispatch的时候其实就是通过参数名查找之前注册过的函数,然后通过反射调用该函数。

Bus结构体里面有几个map成员,在这个项目里面作者定义了3种不同类型的handler,一种是普通的handler,也就是刚才展示的那种,第二种是带上下文的handler,还有一种则是事件订阅用到的handler,我们给一个事件注册多个监听者,当事件触发的时候会依次调用多个监听函数,其实就是一个观察者模式。

//InProcBusdefinesthebusstructure
typeInProcBusstruct{
handlersmap[string]HandlerFunc
handlersWithCtxmap[string]HandlerFunc
listenersmap[string][]HandlerFunc
txMngTransactionManager
}

下面就看看具体的源码,AddHandler方法内容如下:

func(b*InProcBus)AddHandler(handlerHandlerFunc){
handlerType:=reflect.TypeOf(handler)
queryTypeName:=handlerType.In(0).Elem().Name()//获取函数第一个参数的名称,在上面例子里面就是GetAlertByIdQuery
b.handlers[queryTypeName]=handler
}

Dispatch方法的源码如下:

func(b*InProcBus)Dispatch(msgMsg)error{
varmsgName=reflect.TypeOf(msg).Elem().Name()

withCtx:=true
handler:=b.handlersWithCtx[msgName]//根据参数名查找注册过的函数,先查找带Ctx的handler
ifhandler==nil{
withCtx=false
handler=b.handlers[msgName]
ifhandler==nil{
returnErrHandlerNotFound
}
}
varparams=[]reflect.Value{}
ifwithCtx{
//如果查找到的handler是带Ctx的就给个默认的Background的Ctx
params=append(params,reflect.ValueOf(context.Background()))
}
params=append(params,reflect.ValueOf(msg))

ret:=reflect.ValueOf(handler).Call(params)//通过反射机制调用函数
err:=ret[0].Interface()
iferr==nil{
returnnil
}
returnerr.(error)
}

对于AddHandlerCtxDispatchCtx这个2个方法基本上是一样的,只不过多了一个上下文参数,可以拿来做超时控制或者其它用途。

2.订阅和发布

除此之外,还有2个方法AddEventListenerPublish,即事件的订阅和发布。

func(b*InProcBus)AddEventListener(handlerHandlerFunc){
handlerType:=reflect.TypeOf(handler)
eventName:=handlerType.In(0).Elem().Name()
_,exists:=b.listeners[eventName]
if!exists{
b.listeners[eventName]=make([]HandlerFunc,0)
}
b.listeners[eventName]=append(b.listeners[eventName],handler)
}

查看源码可以得知,可以给一个事件注册多个handler函数,而Publish的时候则是依次调用注册的函数,逻辑也不复杂。

func(b*InProcBus)Publish(msgMsg)error{
varmsgName=reflect.TypeOf(msg).Elem().Name()
varlisteners=b.listeners[msgName]

varparams=make([]reflect.Value,1)
params[0]=reflect.ValueOf(msg)

for_,listenerHandler:=rangelisteners{
ret:=reflect.ValueOf(listenerHandler).Call(params)
e:=ret[0].Interface()
ife!=nil{
err,ok:=e.(error)
ifok{
returnerr
}
returnfmt.Errorf("expectedlistenertoreturnanerror,got'%T'",e)
}
}
returnnil
}

这里面有一点不好,所有订阅函数的调用是顺序的,并没有使用协程,所以如果注册了很多个函数,这样效率也不高啊。

3.好处

可能有人会好奇,为什么明明可以直接调用函数就行,为啥非得绕个弯子,整这么复杂?

况且,每次调用都得使用反射机制,性能也不行。

我觉得主要有以下几点:

1.这种写法逻辑清晰,解耦

2.方便单元测试

3.性能不是最大考量,虽然说反射会降低性能

原文标题:Golang 事件系统 Event Bus

文章出处:【微信公众号:Linux爱好者】欢迎添加关注!文章转载请注明出处。

审核编辑:汤梓红
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 总线
    +关注

    关注

    10

    文章

    2706

    浏览量

    87211
  • 开源
    +关注

    关注

    3

    文章

    2985

    浏览量

    41716
  • 代码
    +关注

    关注

    30

    文章

    4555

    浏览量

    66762

原文标题:Golang 事件系统 Event Bus

文章出处:【微信号:LinuxHub,微信公众号:Linux爱好者】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    Golang为何舍弃三元运算符

    golang中不存在?:运算符的原因是因为语言设计者已经预见到三元运算符经常被用来构建一些极其复杂的表达式。虽然使用if进行替代会让代码显得更长,但这毫无疑问可读性更强。
    的头像 发表于 04-03 15:13 132次阅读

    CAN总线的可靠通信是依靠什么机制实现的?

    CAN总线采取多种技术措施来消除外界干扰,确保可靠通信。
    的头像 发表于 01-30 09:50 389次阅读

    如何使用Golang连接MySQL

    首先我们来看如何使用Golang连接MySQL。
    的头像 发表于 01-08 09:42 1883次阅读
    如何使用<b class='flag-5'>Golang</b>连接MySQL

    Golang接口的作用和应用场景

    Golang(Go)作为一门现代的静态类型编程语言,提供了许多强大的特性,其中之一便是接口(interface)。接口是Golang中的一个核心概念,它具有广泛的应用场景,可以帮助开发者实现
    的头像 发表于 12-05 10:44 628次阅读

    基于表驱动的健康监控机制实现方法

    电子发烧友网站提供《基于表驱动的健康监控机制实现方法.pdf》资料免费下载
    发表于 11-06 10:09 0次下载
    基于表驱动的健康监控<b class='flag-5'>机制</b><b class='flag-5'>实现</b>方法

    RTT中的消息同步机制是如何实现的?

    RTT中的消息同步机制是如何实现
    发表于 11-02 07:00

    RTT的任务切换机制是如何实现的?

    RTT中如何实现任务切换机制
    发表于 11-02 06:28

    CAN总线访问与仲裁机制#汽车CAN总线 

    CAN总线
    北汇信息POLELINK
    发布于 :2023年10月20日 15:33:02

    核间通信可能的实现机制

    理解为软件可自由定义的中断模块。 用于在片上处理器之间通信的一种mailbox队列中断机制,mailbox队列中断机制允许软件通过一组寄存器和关联的中断设置和得到信息在二个处理之间建立通信渠道。 核间通信的主要目标是:充分利用硬件提供的
    的头像 发表于 09-13 17:39 814次阅读
    核间通信可能的<b class='flag-5'>实现</b><b class='flag-5'>机制</b>

    Golang泛型的使用

    众所周知很多语言的function 中都支持 key=word 关键字参数, 但 golang 是不支持的, 我们可以利用泛型去简单的实现
    发表于 08-16 12:24 179次阅读

    【芒果派MangoPi MQ Quad】使用Golang点灯

    使用Golang在芒果派上点灯
    的头像 发表于 07-21 14:44 446次阅读
    【芒果派MangoPi MQ Quad】使用<b class='flag-5'>Golang</b>点灯

    CAN总线的位定时与同步机制#汽车CAN总线 

    CAN总线
    北汇信息POLELINK
    发布于 :2023年07月17日 10:57:12

    CAN 总线错误检测机制#can总线

    CAN 总线
    北汇信息POLELINK
    发布于 :2023年05月26日 13:54:03

    单片机消息队列的实现原理和机制

    单片机开发过程中通常会用到“消息队列”,一般实现的方法有多种。 本文给大家分享一下队列实现的原理和机制
    的头像 发表于 05-26 09:50 870次阅读
    单片机消息队列的<b class='flag-5'>实现</b>原理和<b class='flag-5'>机制</b>

    can总线的可靠通信是依靠什么机制实现的?

    can总线的可靠通信是依靠什么机制实现的?
    发表于 05-09 10:35