首页 > 其他分享 >Go每日一库之16:message-bus

Go每日一库之16:message-bus

时间:2023-09-20 09:35:04浏览次数:40  
标签:16 bus level topic 一库 func go message

简介

在一个涉及多模块交互的系统中,如果模块的交互需要手动去调用对方的方法,那么代码的耦合度就太高了。所以产生了异步消息通信。实际上,各种各样的消息队列都是基于异步消息的。不过它们大部分都有着非常复杂的设计,很多被设计成一个独立的软件来使用。今天我们介绍一个非常小巧的异步消息通信库[message-bus](https://github.com/vardius/message-bus),它只能在一个进程中使用。源代码只有一个文件,我们也简单看一下实现。

快速使用

安装:

$ go get github.com/vardius/message-bus

使用:

package main

import (
  "fmt"
  "sync"

  messagebus "github.com/vardius/message-bus"
)

func main() {
  queueSize := 100
  bus := messagebus.New(queueSize)

  var wg sync.WaitGroup
  wg.Add(2)

  _ = bus.Subscribe("topic", func(v bool) {
    defer wg.Done()
    fmt.Println(v)
  })

  _ = bus.Subscribe("topic", func(v bool) {
    defer wg.Done()
    fmt.Println(v)
  })

  bus.Publish("topic", true)
  wg.Wait()
}

这是官网提供的例子,message-bus承担了模块间消息分发的角色。模块 A 和 模块 B 先向message-bus订阅主题(topic),即告诉message-bus对什么样的消息感兴趣。其他模块 C 产生某个主题的消息,通知message-bus,由message-bus分发到对此感兴趣的模块。这样就实现了模块之间的解耦,模块 A、B 和 C 之间不需要知道彼此。

上面的例子中:

  • 首先,调用messagebuss.New()创建一个消息管理器;
  • 其次调用Subscribe()方法向管理器订阅主题;
  • 调用Publish()向管理器发布主题消息,这样订阅该主题的模块就会收到通知。

更复杂的例子

其实很多人会对何时使用这个库产生疑问,message-bus GitHub 仓库中 issue 中至今还躺着这个问题,https://github.com/vardius/message-bus/issues/4。我是做游戏后端开发的,在一个游戏中会涉及各种各样的功能模块,它们需要了解其他模块产生的事件。例如每日任务有玩家升多少级的任务、成就系统有等级的成就、其他系统还可能根据玩家等级执行其他操作...如果硬写的话,最后可能是这样:

func (p *Player) LevelUp() {
  // ...
  p.DailyMission.OnPlayerLevelUp(oldLevel, newLevel)
  p.Achievement.OnPlayerLevelUp(oldLevel, newLevel)
  p.OtherSystem.OnPlayerLevelUp(oldLevel, newLevel)
}

需求一直在新增和迭代,如果新增一个模块,也需要在玩家升级时进行一些处理,除了实现模块自身的OnPlayerLevelUp方法,还必须在玩家的LevelUp()方法调用。这样玩家模块必须清楚地知道其他模块的情况。如果功能模块再多一点,而且由不同的人开发的,那么情况会更复杂。使用异步消息可有效解决这个问题:在升级时我们只需要向消息管理器发布这个升级“消息”,由消息管理器通知订阅该消息的模块。

我们设计的目录结构如下:

game
├── achievement.go
├── daily_mission.go
├── main.go
├── manager.go
└── player.go

其中manager.go负责message-bus的创建:

package main

import (
  messagebus "github.com/vardius/message-bus"
)

var bus = messagebus.New(10)

player.go对应玩家结构(为了简便起见,很多字段省略了):

package main

type Player struct {
  level uint32
}

func NewPlayer() *Player {
  return &Player{}
}

func (p *Player) LevelUp() {
  oldLevel := p.level
  newLevel := p.level+1
  p.level++

  bus.Publish("UserLevelUp", oldLevel, newLevel)
}

achievement.godaily_mission.go分别是成就和每日任务(也是省略了很多无关细节):

// achievement.go
package main

import "fmt"

type Achievement struct {
  // ...
}

func NewAchievement() *Achievement {
  a := &Achievement{}
  bus.Subscribe("UserLevelUp", a.OnUserLevelUp)
  return a
}

func (a *Achievement) OnUserLevelUp(oldLevel, newLevel uint32) {
  fmt.Printf("daily mission old level:%d new level:%d\n", oldLevel, newLevel)
}
// daily_mission.go
package main

import "fmt"

type DailyMission struct {
  // ...
}

func NewDailyMission() *DailyMission {
  d := &DailyMission{}
  bus.Subscribe("UserLevelUp", d.OnUserLevelUp)
  return d
}

func (d *DailyMission) OnUserLevelUp(oldLevel, newLevel uint32) {
  fmt.Printf("daily mission old level:%d new level:%d\n", oldLevel, newLevel)
}

在创建这两个功能的对象时,我们订阅了UserLevelUp主题。玩家在升级时会发布这个主题。

最后main.go驱动整个程序:

package main

import "time"

func main() {
  p := NewPlayer()
  NewDailyMission()
  NewAchievement()

  p.LevelUp()
  p.LevelUp()
  p.LevelUp()

  time.Sleep(1000)
}

注意,由于message-bus是异步通信,为了能看到结果我特意加了time.Sleep,实际开发中不太可能使用Sleep

最后我们运行整个程序:

$ go run .

因为要运行的是一个多文件程序,不能使用go run main.go

实际上,当年我因为苦于模块之间调来调去太麻烦了,自己用 C++ 撸了一个event-managerhttps://github.com/go-quiz/event-manager。思路是一样的。

缺点

message-bus订阅主题时传入一个函数,函数的参数可任意设置,发布时必须使用相同数量的参数,这个限制感觉有点勉强。如果我们传入的参数个数不一致,程序就panic了。我认为可以只用一个参数interface{},传入对象即可。例如,上面的升级事件可以使用EventUserLevelUp的对象:

type EventUserLevelUp struct {
  oldLevel uint32
  newLevel uint32
}

对应地修改一下PlayerLevelUp方法:

func (p *Player) LevelUp() {
  event := &EventUserLevelUp {
    oldLevel: p.level,
    newLevel: p.level+1,
  }
  p.level++

  bus.Publish("UserLevelUp", event)
}

和处理方法:

func (d *DailyMission) OnUserLevelUp(arg interface{}) {
  event := arg.(*EventUserLevelUp)
  fmt.Printf("daily mission old level:%d new level:%d\n", event.oldLevel, event.newLevel)
}

这样一来,我们似乎用不上反射了,订阅者都是func (interface{})类型的函数或方法。感兴趣的可自己实现一下,我 fork 了message-bus,做了这个修改。改动在这里:https://github.com/go-quiz/message-busmessage-bus有测试和性能用例,改完跑一下

标签:16,bus,level,topic,一库,func,go,message
From: https://www.cnblogs.com/arena/p/17716448.html

相关文章

  • 双通道3G/14bit采集+双通道12.6G/16bit回放卡
     UDFMC-704 双通道接收+双通道发射FMC模块满足VITA57.1单宽、导冷规范。模块ADC支持进口AD9689-2000、AD9689-2600、AD6688、AD9208或国产GMS018采集芯片,DAC支持AD9171/AD9172/AD9173/AD9174/AD9175/AD9176回放芯片,输入支持直流或交流耦合方式,输出支持选配放大器。FMC子卡还......
  • CF1808E1 Minibuses on Venus (easy version)
    原题翻译一道数位\(dp\)题记\(S=\sum_{i=1}^{n}{a_i}\),原题即要求是否存在\(i\)满足\(S-a_i\equiva_i(\modK)\)移项得\(S\equiv2a_i(\modK)\)因此我们考虑枚举\(2a_i\)的值记作\(sm\),设\(dp_{i,j,0/1}\)表示前\(i\)个数,和为\(j\),有/没有\(2a_i\modK=sm\)转......
  • POI2016
    P5967Korale题意有n个东西,每个东西有价值,随便选选出的权值和第k小是多少,并输出方案(权值和相同按照选的集合的字典序排列)。题解第一问:求第k小方案的价值考虑贪心,将价值从小到大排序,用二元组(sum,i)描述前i个数中,选出若干数和为sum,其中必选第i个数。利用小根堆不断取出堆顶,并......
  • Windows-Sqlserver2016对指定数据库进行扩容
    前言:之所以会想起来写这一篇文章,是因为工作中正好需要用到,所以记录一下如何对想要的数据库进行扩容操作实际上在处理这种问题之前,我翻阅了许多文章,也没找到自己想要的答案,也正因为如此打算自己写一篇关于扩容数据库的操作文章 搭建实验环境:在扩容之前,我们先创建一个数据库......
  • Go每日一库之15:gojsonq
    简介在日常工作中,每一名开发者,不管是前端还是后端,都经常使用JSON。JSON是一个很简单的数据交换格式。相比于XML,它灵活、轻巧、使用方便。JSON也是RESTfulAPI推荐的格式。有时,我们只想读取JSON中的某一些字段。如果自己手动解析、一层一层读取,这就变得异常繁琐了。特别是在......
  • 16G内存+CPU本地部署ChatGLM2/Baichuan2推理(Windows/Mac/Linux)
    概述本文使用chatglm.cpp对中文大语言模型(LLM)进行量化与推理,支持ChatGLM2-6B、Baichuan2-13B-Chat等模型在CPU环境16G内存的个人电脑上部署,实现类似ChatGPT的聊天功能。支持的操作系统包括Windows、MacOS、Linux等。其中,量化过程需要临时使用一台内存较大的服务器。4bit量化后......
  • AT_arc165_d 题解
    AT_arc165_d[ARC165D]SubstringComparison题解Links洛谷AtCoderDescription给定正整数\(n,m\)和\(m\)个形如\((A_{i},B_{i},C_{i},D_{i})\)的限制条件。判断是否存在一个长度为\(n\)的序列\(P\)满足\(\foralli\in[1,m]\),\(P_{A_{i}\dotsB_{i}}\)字典序......
  • 16_freeRTOS 任务控制函数
    freeRTOS任务控制函数osThreadCreate任务创建函数osThreadTerminateosThreadTerminate(任务对象)任务结束函数task1只打印了一次获取任务IDprintf("id=%d\n",osThreadGetId());printf("id=%d\n",myTask2Handle);两个打印内容相同,都是id任务阻塞osTh......
  • AT_arc165_b 题解
    AT_arc165_b[ARC165B]SlidingWindowSort2题解Links洛谷AtCoderDescription给定正整数\(n,k\)和一个长度为\(n\)的整数\(P\),你需要选择一个长度为\(k\)的区间\([l,l+k-1]\),将这个区间从小到大排序。找到操作后最终字典序最大的排列。\(1\leqk\leqn\l......
  • 【接口工具ApiPost】使用Mock服务前端不用依赖后端接口实现API解耦(16)---(强力推荐本章
    释义:Mock:模仿,仿造。可理解为虚拟环境模拟数据Mock服务:模拟服务器提供API访问服务Mock服务使用路径:接口下面和Header、Query、Body、认证…Mock服务,如下图:使用前准备环境选择必须是Mock环境非mock环境mock服务是不起作用的。环境设置如下:路径:在小眼睛左边默认情况下是有一个官......