Go从入门到精通——结构体(struct)——示例:使用事件系统实现事件的响应和处理

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

  Go 语言可以将类型的方法与普通函数视为一个概念,从而简化方法和函数混合作为回调类型时的复杂性。这个特性和 C# 中的代理(delegate)类似,调用者无须关心谁来支持调用,系统会自动处理是否调用普通函数或类型的方法。
  本节中,首先将用简单的例子了解 Go 语言是如何将方法与函数视为一个概念,接着会实现一个事件系统,事件系统能有效地将事件触发与响应两端代码解耦。

1、方法和函数的统一调用

  本节的例子将让一个结构体的方法(class.Do)的参数和一个普通函数(funcDo)的参数完全一致,也就是方法与函数的签名一致。然后使用与它们签名一致的函数变量(delegate)分别赋值方法与函数,接着调用它们,观察实际效果。

 代码 1-1, 函数代理(具体文件:.../delegate/delegate.go) 

package main

import "fmt"

//声明一个结构体
type class struct {
}

//给结构体添加 Do() 方法,参数为整型,这个方法的功能是打印提示和输入的参数值
func (c *class) Do(v int) {
	fmt.Println("call method do:", v)
}

//声明一个普通函数的 Do(),参数也是整型,功能是打印提示和输入的参数值。
func funcDo(v int) {
	fmt.Println("call method do:", v)
}

func main() {

	//声明一个 delegate 变量,类型为 func(int),与 funcDo 和 class 的 Do()方法的参数一致。
	var delegate func(int)

	//创建结构体实例
	c := new(class)

	//将 c.Do 作为值赋给 delegate 变量。
	delegate = c.Do

	//调用 delegate() 函数,传入 100 的参数。此时会调用 c 实例的 Do() 方法。
	delegate(100)

	//将 funcDo 作为值赋给 delegate。
	delegate = funcDo

	//调用 delegate(), 传入 100 的参数,此时会调用 funcDo() 方法。
	delegate(100)
}

  了解了 Go 语言的这个特性后,我们就可以将这个特性应用在事件系统中。

2、事件系统基本原理

  事件系统可以将事件派发者与事件处理者解耦。例如,网络底层可以生成各种事件,在网络连接上后,网络底层只需将事件派发出去,而不需要关心到底哪些代码来响应连接上的逻辑。或者再比如,你注册、关注或者订阅某“大V”的社交消息后,“大V”发生的任何事件都会通知你,但他并不用了解粉丝们是如何为他喝彩或者疯狂的。

  一个事件系统拥有如下特性:

    • 能够实现事件的乙方,可以根据事件 ID 或 名字 注册对应的事件。
    • 事件发起者,会根据注册信息通知这些注册者。
    • 一个事件可以有多个实现方响应。

 3、事件注册  

  事件系统需要为外部提供一个注册入口。这个注册入口传入注册的事件名称和对应事件名称的响应函数,事件注册的过程就是将事件名称和响应函数关联并保存起来。

 代码 1-2, 注册事件(具体文件:.../eventssys/reg.go) 

//创建一个 map 实例,这个 map 通过事件名(string)关联回调列表([]func(interace{})),
//同一个事件名称可能存在多个事件回调,因此使用回调列表保存。
//回调的函数声明为 func(interface{})。
var eventByName = make(map[string][]func(interface{}))

//注册事件,提供事件名和回调函数。
//提供给外部的通过事件注册响应函数的入口。
func RegisterEvent(name string, callback func(interface{})) {

	//通过名字查找事件列表
	//eventByName 通过事件名(name)进行查询,返回回调列表([]func(interface{}))
	list := eventByName[name]

	//在列表切片中添加函数
	//为同一个事件名称在已经注册的事件回调的列表中再添加一个回调函数。
	list = append(list, callback)

	//将修改后的函数列表设置到 map 的对应事件中。
	eventByName[name] = list
} 

  拥有事件名和事件回调函数列表的关联关系后,就需要开始准备事件调用的入口了。

4、事件调用

  事件调用方和注册方式是事件处理中完全不同的两个角色。事件调用方是事发现场,负责将事件和事件发生的参数通过事件系统派发出去,而不关心事件到底由谁处理;

  事件注册方通过事件系统注册应该响应哪些事件及如何使用回调函数处理这些事件。

  事件调用的详细实现请参考 CallEvent() 函数:

代码 1-3, 调用事件(具体文件:.../eventssys/reg.go) 

//调用事件的入口。提供事件名称name和参数param。
//事件的参数表示描述事件具体的细节,例如门打开的事件触发时,参数可以传入谁进来了。
func CallEvent(name string, param interface{}) {

	//通过注册事件回调的 eventByName 和事件名字查询处理函数列表 list。
	list := eventByName[name]

	//遍历这个事件列表,如果没有找到对应的事件,list将是一个空切片。
	for _, callback := range list {

		//将每个函数回调转入事件参数并调用,就会触发事件实现方的逻辑处理。
		callback(param)
	}
}

5、使用事件系统

  例子中,在 main() 函数中调用事件的 CallEvent 生成 OnSkill 事件,这个 事件有两个处理函数,一个是角色的 OnEvent() 方法,还有一个是函数 GlobalEvent(),详细代码实现过程如下:

  代码 1-3, 使用事件系统(具体文件:.../eventssys/main.go)
package main

import (
	"fmt"
)

//声明一个角色的结构体,本例中,角色也是 OnSkill 事件的响应处理方
type Actor struct {
}

//为角色结构添加一个OnEvent()方法,这个方法拥有 param 参数,类型为 interface{},
//与事件系统的函数(func(interface{}))签名一致。
func (a *Actor) OnEvent(param interface{}) {
	fmt.Println("actor event:", param)
}

//GolobalEvent()函数为全局响应事件。
//有时需要全局进行侦听或者处理一些事件,这里使用普通函数实现全局事件的处理。
func GlobalEvent(param interface{}) {
	fmt.Println("global event:", param)
}

func main() {

	//实例化一个角色
	a := new(Actor)

	//注册一个 OnSkill 事件,实现代码由 a 的 OnEvent 进行处理。也就是 Actor 的 OnEvent()方法。
	RegisterEvent("Onskill", a.OnEvent)

	//再次注册一个 OnSkill 事件,实现代码由 GlobalEvent 进行处理。
	//虽然注册了2次,也注册的同一个名字,但前面注册的事件不会被覆盖,而是被添加到事件系统中,关联 OnSkill 事件的函数列表中。
	RegisterEvent("OnSkill", GlobalEvent)

	//模拟处理事件,通过 CallEvent() 函数传入两个参数,第一个为事件名,第二个为处理函数的参数。
	CallEvent("OnSkill", 100)
}

6、完整程序

package main

import (
	"fmt"
)

//声明一个角色的结构体,本例中,角色也是 OnSkill 事件的响应处理方
type Actor struct {
}

//为角色结构添加一个OnEvent()方法,这个方法拥有 param 参数,类型为 interface{},
//与事件系统的函数(func(interface{}))签名一致。
func (a *Actor) OnEvent(param interface{}) {
	fmt.Println("actor event:", param)
}

//GolobalEvent()函数为全局响应事件。
//有时需要全局进行侦听或者处理一些事件,这里使用普通函数实现全局事件的处理。
func GlobalEvent(param interface{}) {
	fmt.Println("global event:", param)
}

//创建一个 map 实例,这个 map 通过事件名(string)关联回调列表([]func(interace{})),
//同一个事件名称可能存在多个事件回调,因此使用回调列表保存。
//回调的函数声明为 func(interface{})。
var eventByName = make(map[string][]func(interface{}))

//注册事件,提供事件名和回调函数。
//提供给外部的通过事件注册响应函数的入口。
func RegisterEvent(name string, callback func(interface{})) {

	//通过名字查找事件列表
	//eventByName 通过事件名(name)进行查询,返回回调列表([]func(interface{}))
	list := eventByName[name]

	//在列表切片中添加函数
	//为同一个事件名称在已经注册的事件回调的列表中再添加一个回调函数。
	list = append(list, callback)

	//将修改后的函数列表设置到 map 的对应事件中。
	eventByName[name] = list
}

//调用事件的入口。提供事件名称name和参数param。
//事件的参数表示描述事件具体的细节,例如门打开的事件触发时,参数可以传入谁进来了。
func CallEvent(name string, param interface{}) {

	//通过注册事件回调的 eventByName 和事件名字查询处理函数列表 list。
	list := eventByName[name]

	//遍历这个事件列表,如果没有找到对应的事件,list将是一个空切片。
	for _, callback := range list {

		//将每个函数回调转入事件参数并调用,就会触发事件实现方的逻辑处理。
		callback(param)
	}
}

func main() {

	//实例化一个角色
	a := new(Actor)

	//注册一个 OnSkill 事件,实现代码由 a 的 OnEvent 进行处理。也就是 Actor 的 OnEvent()方法。
	RegisterEvent("Onskill", a.OnEvent)

	//再次注册一个 OnSkill 事件,实现代码由 GlobalEvent 进行处理。
	//虽然注册了2次,也注册的同一个名字,但前面注册的事件不会被覆盖,而是被添加到事件系统中,关联 OnSkill 事件的函数列表中。
	RegisterEvent("Onskill", GlobalEvent)

	//模拟处理事件,通过 CallEvent() 函数传入两个参数,第一个为事件名,第二个为处理函数的参数。
	CallEvent("Onskill", 100)
}

  运行结果输出:

Starting: D:\go-testfiles\bin\dlv.exe dap --check-go-version=false --listen=127.0.0.1:52225 from d:\go-testfiles\eventsys
DAP server listening at: 127.0.0.1:52225
Type 'dlv help' for list of commands.
actor event: 100
global event: 100
Process 18960 has exited with status 0
Detaching
dlv dap (14976) exited with code: 0

  结果演示,角色和全局的事件会按注册顺序顺序地触发。

  一般来说,事件系统不保证同一个事件实现方多个函数列表中的调用顺序,事件系统认为所有实现函数都是平等等。也就是说,无论例子中的 a.OnEvent 先注册,还是 GlobalEvent() 函数先注册,最终谁先被调用,都是无所谓的,开发者不应该去关注和要求保证调用的顺序。 

posted @ 2022-05-30 18:48  左扬  阅读(232)  评论(0编辑  收藏  举报
levels of contents