听说Golang挺好用的,想着来体验一下。写个简单的程序,同时记录一下学习笔记。
实现功能描述
这次使用发布订阅模式,实现事件驱动器。
前端的同学肯定知道下面这段代码,监听鼠标移动事件。
- 其中mousemove可以认为我们订阅的topic
- (event) => {} 是事件回调函数
- 发布者由浏览器系统担任
windows.addEventListener("mousemove", (event) => {
//处理鼠标移动事件
})
事件总线(Event bus),通过消息订阅和发布的模式,对两端的业务进行解耦。
事件总线的使用
//创建事件总线
bus := NewChanEventBus()
//创建订阅者
//创建事件管道,管道不能为空,否则事件会被丢弃
ch := make(EventChannel, 64)
bus.Subscribe("topic", ch)
//发布两个事件
bus.Publish("topic", "hello world!!!")
bus.Publish("topic", "hello golang!!!")
//处理订阅的消息事件
go func() {
for item := range ch {
//处理topic事件
fmt.Printf("sub: %#v\n", item)
}
}()
具体实现
定义消息体
- EventItem 是单个消息体的结构。在此消息体的Payload简单使用string,用json序列化即可。实际使用可根据需求替换。
- EventChannel 采用golang的管道(chan)来派发给订阅者。
//单个消息
type EventItem struct {
Payload string
Topic string
}
//消息管道
type EventChannel chan *EventItem
定义事件总线
事件总线主要用来驱动整个事件的触发
- 因为golang相较于js更加灵活,回调函数不一定在同一个Goroutine中执行,因此稍加修改,采用chan的模式。
- Trigger 为发布者用来触发事件时调用
// 事件总线
type EventBus interface {
Subscribe(topic string, ch EventChannel) //订阅消息
UnSubscribe(topic string, ch EventChannel) //取消订阅
Publish(topic string, payload string) error //发布消息
Trigger(topic string, item *EventItem) //用于触发消息订阅事件
}
定义发布者(pub)
// 发布者
type EventPublisher interface {
Publish(topic string, item *EventItem, bus EventBus) error //发布消息
}
定义EventBus的实现基类
EventBusBase:
- subscriber 是以topic作为key,订阅者通道slice作为value。 当有事件时,就可以派发给对应topic下的管道(chan)
- lock 用于保证多线程下的事件触发安全
- publisher 发布者可以定义各种模式的事件发布,例如最简单的发布时直接触发,或者使用Redis或者RabbitMQ作为消息发布者
//事件总线实现
type EventBusBase struct {
subscriber map[string][]EventChannel //订阅者,[topic] => 订阅者列表
lock sync.RWMutex //读写锁
publisher EventPublisher //发布者
}
// 订阅消息
func (s *EventBusBase) Subscribe(topic string, ch EventChannel) {
s.lock.Lock()
defer s.lock.Unlock()
if slice, ok := s.subscriber[topic]; ok {
if utils.ArrayIndex(ch, slice) != -1 {
//如果已经订阅了,则不重复订阅
return
}
s.subscriber[topic] = append(slice, ch)
} else {
s.subscriber[topic] = []EventChannel{ch}
}
}
// 取消订阅
func (s *EventBusBase) UnSubscribe(topic string, ch EventChannel) {
s.lock.Lock()
defer s.lock.Unlock()
if slice, ok := s.subscriber[topic]; ok {
if index := utils.ArrayIndex(ch, slice); index != -1 {
//找到订阅者,删除
s.subscriber[topic] = append(slice[:index], slice[index+1:]...)
}
}
}
// 发布消息
func (s *EventBusBase) Publish(topic string, payload string) error {
if s.publisher == nil {
return errors.New("publisher is nil")
}
return s.publisher.Publish(topic, &EventItem{
Topic: topic,
Payload: payload,
}, s)
}
// 用于触发消息订阅事件
func (s *EventBusBase) Trigger(topic string, item *EventItem) {
s.lock.RLock()
defer s.lock.RUnlock()
if slice, ok := s.subscriber[topic]; ok {
for _, ch := range slice {
select {
case ch <- item:
default:
//如果通道中消息已满,则丢弃,防止阻塞。
log.Println("write chan full!!!")
}
}
}
}
创建直接触发的发布者
创建一个采用golang chan驱动的发布者,publish之后,直接就Trigger触发订阅回调事件。
//创建直接触发的事件总线
func NewChanEventBus() EventBus {
publisher := &EventChanPublisher{}
bus := &EventBusBase{
subscriber: make(map[string][]EventChannel),
publisher: publisher,
}
return bus
}
//实现直接触发的发布者
type EventChanPublisher struct {
}
func (s *EventChanPublisher) Publish(topic string, item *EventItem, bus EventBus) error {
bus.Trigger(topic, item)
return nil
}
工具函数
package utils
func ArrayIndex[T comparable](needle T, hystack []T) int {
for i, item := range hystack {
if needle == item {
return i
}
}
return -1
}
使用
//创建事件总线
bus := NewChanEventBus()
//创建事件管道,管道不能为空,否则事件会被丢弃
ch := make(EventChannel, 64)
//发布两个事件
err := bus.Publish("topic", "hello world!!!")
if err != nil {
panic(err)
}
err := bus.Publish("topic", "hello golang!!!")
if err != nil {
panic(err)
}
go func() {
for item := range ch {
//处理topic事件
fmt.Printf("sub: %#v\n", item)
}
}()
time.Sleep(time.Second)
完整代码
总结
直接触发方式的事件触发器,存在以下几个问题:
- 发布者和订阅者必须在同一个进程中,无法跨进程,或者跨服务器使用。 (后面用redis作为发布者来解决)
- 事件会受限于订阅者的管道,当管道满的时候会被丢弃。 这个有什么好的解决方案?