golang学习-发布订阅(一)

342 阅读4分钟

听说Golang挺好用的,想着来体验一下。写个简单的程序,同时记录一下学习笔记。

实现功能描述

这次使用发布订阅模式,实现事件驱动器。

前端的同学肯定知道下面这段代码,监听鼠标移动事件。

  • 其中mousemove可以认为我们订阅的topic
  • (event) => {} 是事件回调函数
  • 发布者由浏览器系统担任
windows.addEventListener("mousemove", (event) => {
    //处理鼠标移动事件
})

事件总线(Event bus),通过消息订阅和发布的模式,对两端的业务进行解耦。

image.png

事件总线的使用


//创建事件总线
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作为发布者来解决)
  • 事件会受限于订阅者的管道,当管道满的时候会被丢弃。 这个有什么好的解决方案?