Go Event

JunChow520 · · 2632 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

事件驱动架构是计算机科学中一种高度可扩展的范例,能实现多方系统的异步处理。

事件是由事件源触发并由事件处理消费

事件系统可以将事件派发者和事件处理者进行解耦,一个完整的事件系统需要拥有如下特性:

  • 实现事件的一方可根据事件ID或名字注册对应的事件
  • 事件发起者可根据注册信息通知注册者
  • 一个事件可以具有多个实现方响应
事件系统

事件注册

事件系统需要为外部提供一个注册的入口,注册入口传入注册事件名称和对应事件的响应函数后,将事件名称和响应函数关联保存。

//实例化字符串映射函数切片的事件
var events = make(map[string][]func(interface{}))

//RegisterEvent 注册事件
func RegisterEvent(name string, callback func(interface{})) {
    list := events[name]          //通过键名查找事件列表
    list = append(list, callback) //在列中切片中添加函数
    events[name] = list           //将修改后的事件列表保存回去
}

事件调用

事件调用和注册是事件处理中完全不同的角色,事件调用是事发现场,负责将事件和事件发生的参数通过事件系统派发出去,同时无需关心事件到底会由谁来处理。事件注册则通过事件系统注册应该响应哪些事件以及如何使用回调函数来处理事件。

//CallEvent 调用事件
func CallEvent(name string, param interface{}) {
    list := events[name] //通过键名查询事件列表
    //遍历事件列表中所有回调函数
    for _, callback := range list {
        callback(param) //传入参数调用回调函数
    }
}

使用事件系统实现事件的响应和处理

Golang中可以将类型的方法和函数视为同一个概念,从而简化方法和函数缓和作为回调类型时的复杂性,这个特性和C#中的代理(delegate)类似,调用者无需关心谁来支持调用,系统会自动处理是否调用函数和类型的方法。

package main

import (
    "fmt"
    "time"
)

//EventHandler 事件处理器 
type EventHandler func(event Event)//声明函数回调

//EventListener 事件监听器
type EventListener struct {
    Handler EventHandler //事件处理器
}

//NewEventListener 创建事件监听器
func NewEventListener(handler EventHandler) *EventListener {
    listener := new(EventListener)
    listener.Handler = handler
    return listener
}

//IEventDispatcher 事件调度接口
type IEventDispatcher interface {
    AddListener(eventType string, listener *EventListener)         //添加事件监听
    RemoveListener(eventType string, listener *EventListener) bool //移除事件监听
    HasListener(eventType string) bool                             //是否包含事件
    DispatchEvent(event Event) bool                                //事件派发
}

//Event 事件类型基类
type Event struct {
    Type   string           //事件类型
    Target IEventDispatcher //事件触发器实例
    Data   interface{}      //事件携带数据源
}

//Clone 克隆事件
func (e *Event) Clone() *Event {
    evt := new(Event)
    evt.Type = e.Type
    evt.Target = e.Target
    return evt
}

//ToString 打印输出
func (e *Event) ToString() string {
    return fmt.Sprintf("Event Type %v", e.Type)
}

//NewEvent 创建事件
func NewEvent(eventType string, data interface{}) Event {
    return Event{Type: eventType, Data: data}
}

//EventSaver 事件调度器中存放的单元
type EventSaver struct {
    Type      string
    Listeners []*EventListener
}

//EventDispatcher 事件调度器/事件分发器
type EventDispatcher struct {
    Savers []*EventSaver
}

//AddListener 事件调度器 添加监听
func (ed *EventDispatcher) AddListener(eventType string, listener *EventListener) {
    //循环遍历判断是否存在
    for _, saver := range ed.Savers {
        if saver.Type == eventType {
            saver.Listeners = append(saver.Listeners, listener)
            return
        }
    }
    saver := &EventSaver{Type: eventType, Listeners: []*EventListener{listener}}
    ed.Savers = append(ed.Savers, saver)
}

//RemoveListener 事件调度器 移除监听
func (ed *EventDispatcher) RemoveListener(eventType string, listener *EventListener) bool {
    for _, saver := range ed.Savers {
        saverType := saver.Type
        if saverType != eventType {
            continue
        }
        saverListeners := saver.Listeners
        for index, item := range saverListeners {
            if item != listener {
                continue
            }
            saver.Listeners = append(saverListeners[:index], saverListeners[index+1:]...)
            return true
        }
    }
    return false
}

//HasListener 事件调度器 是否存在某类监听
func (ed *EventDispatcher) HasListener(eventType string) bool {
    for _, saver := range ed.Savers {
        if saver.Type == eventType {
            return true
        }
    }
    return false
}

// DispatchEvent 事件调度器 事件派发
func (ed *EventDispatcher) DispatchEvent(event Event) bool {
    for _, saver := range ed.Savers {
        if saver.Type == event.Type {
            for _, listener := range saver.Listeners {
                event.Target = ed
                listener.Handler(event)
            }
            return true
        }
    }
    return false
}

//NewEventDispatcher 创建事件调度器
func NewEventDispatcher() *EventDispatcher {
    return new(EventDispatcher)
}

//EventTest 测试事件
const EventTest = "event_test"

//test 事件处理函数
func test(event Event) {
    fmt.Println(event.ToString())
}
func main() {
    event := NewEvent(EventTest, nil)
    dispatcher := NewEventDispatcher()
    listener := NewEventListener(test)
    dispatcher.AddListener(EventTest, listener)
    time.Sleep(time.Second * time.Duration(2))
    dispatcher.DispatchEvent(event)
}

观察者模式

观察者模式定义了对象间一种一对多的依赖关系,每当一个对象状态发生改变时,所有依赖于它的对象都会得到通知并被自动更新。

发布订阅模式主要由两类角色构成

  • 发布方(Publisher):被观察者,当状态改变时负责通知所有订阅者
  • 订阅方(Subscriber):观察者,订阅事件并对接收到的事件进行处理

发布订阅模式有两种实现方式

  • 简单实现:由发布方维护一个订阅者列表,当状态改变时循环遍历列表通知订阅者。
  • 事件委托:由发布方定义事件委托,由订阅方实现委托。

总体而言,发布订阅模式中会涉及到两个核心的关键点,它们分别是通知和更新。当被观察者状态改变时需通知观察者做出相应的更新,因此发布订阅模式实际上要解决的问题是当对象改变时需要通知其它对象做出相应改变的问题。

发布订阅模式流程

事件总线

事件总线是对观察者模式的一种实现,是一种集中式事件处理机制,它允许不同的组件之间进行必须通信而又无需相互依赖,以达到解耦的目的。

事件总线

事件总线采用观察者模式,发布者发布数据,感兴趣的订阅者可以监听这些数据并基于这些数据做出处理,发布者和订阅者之间的关系是松耦合的。发布者将数据事件发布到事件总线后,事件总线负责将它们发送给订阅者。

传统实现事件总线的方式会涉及到回调,订阅者通过实现接口后事件总线通过接口传播数据。使用Go的并发模型时大多数事件可采用channel信道来代替回调。

发布订阅模式并非最终的目的,最终的目的是一个集中式的事件处理机制,以保障各个模块之间不具有依赖。为了实现这个模式一般都需要经过三个步骤

  1. 事件发布方定义事件委托
  2. 事件订阅方定义事件处理逻辑(事件处理器)
  3. 显式地订阅事件
package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

//Event 定义事件的数据机构
type Event struct {
    Topic string
    Data  interface{}
}

//EventChannel 存储能够接收事件的信道
type EventChannel chan Event

//EventChannels 存储事件信道的切片
type EventChannels []EventChannel

//EventBus 事件总线
type EventBus struct {
    //存储订阅者感兴趣的特定主题
    Subscribers map[string]EventChannels
    Locker      sync.RWMutex
}

//Publish 发布事件
func (eb *EventBus) Publish(topic string, data interface{}) {
    //添加读写互斥锁
    eb.Locker.Lock()
    defer eb.Locker.Unlock()
    //判断订阅的主题是否存在
    chans, ok := eb.Subscribers[topic]
    if ok {
        channels := append(EventChannels{}, chans...)
        //使用异步执行单元来避免阻塞发布者
        go func(event Event, channels EventChannels) {
            //遍历信道分发事件
            for _, ch := range channels {
                ch <- event
            }
        }(Event{topic, data}, channels)
    }
}

//Subscribe 订阅主题
func (eb *EventBus) Subscribe(topic string, channel EventChannel) {
    //添加读写互斥锁
    eb.Locker.Lock()
    defer eb.Locker.Unlock()
    //判断事件是否存在
    chans, ok := eb.Subscribers[topic]
    if !ok {
        chans = []EventChannel{}
    }
    eb.Subscribers[topic] = append(chans, channel)
}

//Print 打印事件总线
func (eb *EventBus) Print() {
    for key, val := range eb.Subscribers {
        fmt.Printf("eventbus subscribe: topic = %v, channels = %v\n", key, val)
    }
}

func (eb *EventBus) Echo(channelName string, data Event) {
    fmt.Printf("channel = %v, topic = %v, channel = %v\n", channelName, data.Topic, data.Data)
}

//NewEventBus 创建事件总线
func NewEventBus() *EventBus {
    return &EventBus{map[string]EventChannels{}, sync.RWMutex{}}
}

var eb = NewEventBus()

func pub(topic string, data interface{}) {
    //发布主题
    for {
        eb.Publish(topic, data)
        time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
    }
}

func main() {
    //创建信道
    ch1 := make(chan Event)
    ch2 := make(chan Event)
    ch3 := make(chan Event)
    //订阅主题
    eb.Subscribe("topic1", ch1)
    eb.Subscribe("topic2", ch2)
    eb.Subscribe("topic3", ch3)
    //发布
    go pub("topic1", "welcome topic1")
    go pub("topic2", "welcome topic2")

    for {
        select {
        case evt := <-ch1:
            eb.Echo("ch1", evt)
        case evt := <-ch2:
            eb.Echo("ch2", evt)
        case evt := <-ch3:
            eb.Echo("ch3", evt)
        }
    }
}

基于主题的事件

  • 发布者发布到主体后订阅者可以收听到它们

事件总线是实现基于事件驱动模型的方式之一,事件总线又称为Broker Topology即基于主题的事件。时间发布方将事件消息发送到一个中心的broker上,事件订阅方向中心的broker订阅和接收事件,然后再处理接收到的事件。当然,订阅者不仅可以接收和消费事件,它们本身也可以创建事件并将其发送到事件总线上。

Broker Topology

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:JunChow520

查看原文:Go Event

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2632 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传