Go语言可以将类型的方法与普通函数视为一个概念,从而简化方法和函数混合作为回调类型时的复杂性。这个特性和 C# 中的代理(delegate)类似,调用者无须关心谁来支持调用,系统会自动处理是否调用普通函数或类型的方法。
本节中,首先将用简单的例子了解 Go语言是如何将方法与函数视为一个概念,接着会实现一个事件系统,事件系统能有效地将事件触发与响应两端代码解耦。
本节的例子将让一个结构体的方法(class.Do)的参数和一个普通函数(funcDo)的参数完全一致,也就是方法与函数的签名一致。然后使用与它们签名一致的函数变量(delegate)分别赋值方法与函数,接着调用它们,观察实际效果。详细实现请参考如下代码。
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 function do:", v)
}
func main() {
// 声明一个函数回调
var delegate func(int)
// 创建结构体实例
c := new(class)
// 将回调设为c的Do方法
delegate = c.Do
// 调用
delegate(100)
// 将回调设为普通函数
delegate = funcDo
// 调用
delegate(100)
}
代码说明如下:
运行代码,输出如下:
这段代码能运行的基础在于:无论是普通函数还是结构体的方法,只要它们的签名一致,与它们签名一致的函数变量就可以保存普通函数或是结构体方法。了解了 Go语言的这一特性后,我们就可以将这个特性用在事件中。
事件系统可以将事件派发者与事件处理者解祸。例如,网络底层可以生成各种事件,在网络连接上后,网络底层只需将事件派发出去,而不需要关心到底哪些代码来响应连接上的逻辑。或者再比如,你注册、关注或者订阅某“大V”的社交消息后,“大V”发生的任何事件都会通知你,但他并不用了解粉丝们是如何为她喝彩或者疯狂的。如下图所示为事件系统基本原理图。
一个事件系统拥有如下特性:
事件系统需要为外部提供一个注册入口。这个注册入口传入注册的事件名称和对应事件名称的响应函数,事件注册的过程就是将事件名称和响应函数关联并保存起来,详细实现代码如下所示。
// 实例化一个通过字符串映射函数切片的map
var eventByName = make(map[string][]func(interface{}))
// 注册事件,提供事件名和回调函数
func RegisterEvent(name string, callback func(interface{})) {
// 通过名字查找事件列表
list := eventByName[name]
// 在列表切片中添加函数
list = append(list, callback)
// 将修改的事件列表切片保存回去
eventByName[name] = list
}
代码说明如下:
拥有事件名和事件回调函数列表的关联关系后,就需要开始准备事件调用的入口了。
事件调用方和注册方是事件处理中完全不同的两个角色。事件调用方是事发现场,负责将事件和事件发生的参数通过事件系统派发出去,而不关心事件到底由谁处理;事件注册方通过事件系统注册应该响应哪些事件及如何使用回调函数处理这些事件。事件调用的详细实现代码如下所示。
// 调用事件
func CallEvent(name string, param interface{}) {
// 通过名字找到事件列表
list := eventByName[name]
// 遍历这个事件的所有回调
for _, callback := range list {
// 传入参数调用回调
callback(param)
}
}
代码说明如下:
事件系统应该具备的事件注册和调用已经实现,下面将会使用事件系统把实际的事发现场和事件处理方联系起来。
例子中,在 main() 函数中调用事件系统的 CallEvent 生成 OnSkill 事件,这个事件有两个处理函数,一个是角色的 OnEvent() 方法,还有一个是函数 GlobalEvent(),详细实现代码如下所示。
package main
import "fmt"
// 声明角色的结构体
type Actor struct {
}
// 为角色添加一个事件处理函数
func (a *Actor) OnEvent(param interface{}) {
fmt.Println("actor event:", param)
}
// 全局事件
func GlobalEvent(param interface{}) {
fmt.Println("global event:", param)
}
func main() {
// 实例化一个角色
a := new(Actor)
// 注册名为OnSkill的回调
RegisterEvent("OnSkill", a.OnEvent)
// 再次在OnSkill上注册全局事件
RegisterEvent("OnSkill", GlobalEvent)
// 调用事件,所有注册的同名函数都会被调用
CallEvent("OnSkill", 100)
}
代码说明如下:
整个例子运行结果如下:
结果演示,角色和全局的事件会按注册顺序顺序地触发。
一般来说,事件系统不保证同一个事件实现方多个函数列表中的调用顺序,事件系统认为所有实现函数都是平等的。也就是说,无论例子中的 a.OnEvent 先注册,还是 GlobalEvent() 函数先注册,最终谁先被调用,都是无所谓的,开发者不应该去关注和要求保证调用的顺序。一个完善的事件系统还会提供移除单个和所有事件的方法。