上篇文章介绍了两个设计模式,分别是单例模式和简单工厂模式,里面也引出了一些常用的Go编程特性,例如包内函数和变量的私有化,sync.Once,协程,chan,等待组,接收者函数,接口类型interface,空结构体struct{}等等,那么我们继续通过设计模式来感受Go语法的独特之处。今天要介绍的是设计模式中的观察模式,也就是订阅发布模式,它实现方式有两种,一种是不考虑任何通用性、复用性的简易实现版本,另一种是event bus事件总线框架实现的版本,这两种模式用到的Go特性如下:make与切片、for与range、lock、defer和reflect,好啦,让我来分别详细说明一哈。
观察者模式observer
Go特性关键词:make与切片,for与range,可变参数...(三个点)
观察者模式另一个名字订阅发布模式大家一定非常熟悉,比如说最近新款iPhone上线了,由于非常火爆肯定会有小伙伴们遇到没货的情况,那么这个时候电商一般会有一个订阅模式,比如说来货了会通知你,那么这个就是观察者模式。实现起来也比较简单,可以想象到电商平台一定要维护一个观察者的链表,当来货的时候会遍历链表通知用户,每个用户都会有一个通知后的hook函数。
好的,那么上述的实现自然要涉及链表和遍历的操作 ,Go提供了一种叫切片的东西slice,为处理同类型数据序列提供一个方便而高效的方式。
swift
//切片的定义方式有两种
nums1 := []int{1, 2, 3, 4, 5}
//用make元素类型,len当前长度, cap最大容量
//make仅用来分配及初始化类型为 slice、map、chan 的数据。new 可分配任意类型的数据.
//new 分配返回的是指针,即类型 *Type。make 返回引用,即 Type.
//new 分配的空间被清零, make 分配空间后,会进行初始化 ,Go就是这么严谨
nums2 := make([]int, 8, 10)
fmt.Printf("%v\n%v\n", nums1, nums2)
//nums1:[1 2 3 4 5]
//nums2:[]
//切片的四大操作
//1.深拷贝copy
copy(nums2, nums1)
fmt.Printf("nums1:%v\nnums2:%v\n", nums1, nums2)
nums1[0] = 3
fmt.Printf("nums1:%v\nnums2:%v\n", nums1, nums2)
//nums1:[1 2 3 4 5]
//nums2:[1 2 3 4 5 0 0 0]
//nums1:[3 2 3 4 5]
//nums2:[1 2 3 4 5 0 0 0]
//2.直接赋值是浅拷贝
nums2 = nums1
fmt.Printf("nums1:%v\nnums2:%v\n", nums1, nums2)
nums1[0] = 3
fmt.Printf("nums1:%v\nnums2:%v\n", nums1, nums2)
//nums1:[1 2 3 4 5]
//nums2:[1 2 3 4 5]
//nums1:[3 2 3 4 5]
//nums2:[3 2 3 4 5]
//3.Append,末尾追加元素
nums2 = append(nums2, 1, 2, 3, 4, 5)
fmt.Printf("nums1:%v\nnums2:%v\n", nums1, nums2)
//nums1:[1 2 3 4 5]
//nums2:[0 0 0 0 0 0 0 0 1 2 3 4 5]
//大家可以看到尾部追加元素后是可以超过当前的最大容量的
//我们可以打印出来,现在这个切片的长度和容量
fmt.Printf("nums1:%v\nnums2:%v\n", nums1, nums2)
fmt.Printf("cap:%v\nlen:%v\n", cap(nums2), len(nums2))
//nums1:[1 2 3 4 5]
//nums2:[0 0 0 0 0 0 0 0 1 2 3 4 5]
//cap:20
//len:13
//可见容量确实变大了,即当append后的长度大于cap时,则会分配一块更大的区域来容纳新的底层数组
//因此,预先设置合适的cap的能够获得最好的性能
//4.Delete 删除元素
//切片没有指定位置删除的函数,我们可以用曲线救国以一下,可以用:就不带你玩的思路
//删除第2个
copy(nums1[1:], nums1[2:])
nums1[len(nums1)-1] = 0
nums1 = nums1[:len(nums1)-1]
fmt.Printf("nums1:%v\n\n", nums1)
//nums1:[1 3 4 5]
//5.Insert 某一个位置处新增
//还是得曲线救国,在某处连续的append
//三个点代表可变长度的参数,即代表append会追加多个元素,你要是不指定默认就追加一个的,但是你的参数又是一个切片
//所以编译会失败的,必须加...告诉编译器是变长的参数
nums1 = append(nums1[:1], append([]int{2}, nums1[1:]...)...)
fmt.Printf("nums1:%v\n\n", nums1)
//nums1:[1 2 2 3 4 5]
好啦,有了这一系列的切片的操作秘籍,我们开始写观察者模式:
go
//code
package observer
import "fmt"
type ElectronicBusiness interface {
Register(user Subscriber)
Remove(user Subscriber)
Notify(msg string)
}
type Subscriber interface {
Update(msg string)
}
type JD struct {
subscribers []Subscriber
}
func (jd *JD) Register(user Subscriber) {
jd.subscribers = append(jd.subscribers, user)
}
func (jd *JD) Remove(user Subscriber) {
//如果想使用 range 同时迭代下标和值,则需要将切片/数组的元素改为指针,才能不影响性能。
//因为range的值是创建了一个拷贝的
for i := range jd.subscribers {
if jd.subscribers[i] == user {
jd.subscribers = append(jd.subscribers[:i], jd.subscribers[i+1:]...)
}
}
}
func (jd *JD) Notify(msg string) {
for i := range jd.subscribers {
jd.subscribers[i].Update(msg)
}
}
type XiaoMing struct {
times int
}
func (x *XiaoMing) Update(msg string) {
if x.times == 0 {
fmt.Printf("%s, XiaoMing:直接从我的黑卡里扣\n", msg)
} else {
fmt.Printf("%s, XiaoMing:买过了,取消订阅\n", msg)
}
x.times += 1
}
type XiaoLi struct{}
func (x *XiaoLi) Update(msg string) {
fmt.Printf("%s, XiaoLi:算了不要了,我的Nokia还能再战2年\n", msg)
}
事件总线event bus
Go特性关键词:lock,defer,reflect
上一个版本我们做的比较简单,通知用户的逻辑都默认放在了服务端,这是不符合实际场景使用的,首先用户可以订阅多个事件,比如手机或者牛奶到货或降价等等,其次可以任意指定某个事件的回调函数,比如说降价了给我打电话,到货了直接帮我加到购物车中等等,这些订阅通知方式都是用户可以主导的。
这里面就涉及到了两个问题,第一,既然用户可以设置自己的回调函数的话,那么我们怎么通过某种结构将这些函数存起来呢?对于C语言中那种函数入参和返回值一样的话,我们可以用函数指针类型代替,那对于完全不同的函数入参和返回值类型的话,我们应该怎么办呢?这就涉及到了Golang的语法reflect反射,它可以帮助我们在函数运行时动态获取对象的类型和值,我们举个栗子:
go
package main
import (
"fmt"
"reflect"
)
func Test(i interface{}) {
//反射获取类型
var t = reflect.TypeOf(i)
fmt.Println("类型:", t)
//反射数据值
var v = reflect.ValueOf(i)
fmt.Println("值:", v)
if reflect.TypeOf(i).Kind() == reflect.Func {
reflect.ValueOf(i).Call(make([]reflect.Value, 0))
}
}
第二,多用户是可以同时订阅一个事件的,这就意味着我们用链表存取用户通知的回调函数时,会有一个并发的考虑,那么我们改动这个链表的时就需要加锁,当处理完成后需要解锁,如果忘记解锁会直接BBQ,对于Go语言有一个特别方便的关键字叫defer,字面意思是调用后延迟执行,一般用于释放资源和连接、关闭文件、释放锁等,这就和C++的析构函数很像。defer用法非常方便,我们举个栗子:
go
func ReadFile(filename string) ([]byte, error) {
//打开文件
f, err := os.Open(filename)
if err != nil {
return nil, err
}
//一会读完文件帮我关下灯,谢谢
defer f.close()
return ReadAll()
}
下面开始正式编码,首先把架子搭出来:
go
package zhihueventbus
import (
"reflect"
"sync"
)
type BusSubscriber interface {
//订阅
Subscribe(product string, fn interface{}) error
//取消订阅
Unsubscribe(product string, handler interface{}) error
}
type BusPublisher interface {
Publish(product string, args ...interface{})
}
type Bus interface {
BusSubscriber
BusPublisher
}
type EventBus struct {
handlers map[string][]reflect.Value // 哈希map 订阅的产品->一系列的通知函数
lock sync.Mutex // a lock for the map
}
func NewBus() Bus {
b := &EventBus{
make(map[string][]*eventHandler),
sync.Mutex{},
}
return Bus(b)
}
func (bus *EventBus) Subscribe(product string, fn interface{}) error {
return nil
}
func (bus *EventBus) SubscribeOnce(product string, fn interface{}) error {
return nil
}
func (bus *EventBus) Unsubscribe(product string, fn interface{}) error {
return nil
}
func (bus *EventBus) Publish(product string, args ...interface{}) {
}
接下来开始实现各个函数:
go
func (bus *EventBus) Subscribe(product string, fn interface{}) error {
bus.lock.Lock()
// map锁了,一会给我解开
defer bus.lock.Unlock()
if !(reflect.TypeOf(fn).Kind() == reflect.Func) {
return fmt.Errorf("%s is not of type reflect.Func", reflect.TypeOf(fn).Kind())
}
// 追加用户通知的回调函数
bus.handlers[product] = append(bus.handlers[product], reflect.ValueOf(fn))
return nil
}
func (bus *EventBus) Unsubscribe(product string, fn interface{}) error {
bus.lock.Lock()
defer bus.lock.Unlock()
// 产品需要被订阅过,且目前至少还有一个用户还再订阅
delIdx := -1
if _, ok := bus.handlers[product]; ok && len(bus.handlers[product]) > 0 {
// 由于是删除,首先要遍历链表得到这个通知函数的位置,然后将它后面的元素前移来覆盖
if _, ok := bus.handlers[product]; ok {
for idx, handler := range bus.handlers[product] {
// 类型一样,且地址一致
if handler.Type() == reflect.ValueOf(fn).Type() &&
handler.Pointer() == reflect.ValueOf(fn).Pointer() {
delIdx = idx
break
}
}
}
if delIdx != -1 {
handlerLen := len(bus.handlers[product])
//后面往前挪
copy(bus.handlers[product][delIdx:], bus.handlers[product][delIdx+1:])
//最后一个置空, reflect.Zerok可以获取表示指定类型的零值的 Value
bus.handlers[product][handlerLen-1] = reflect.Zero(reflect.TypeOf(fn))
//重新赋值,这样长度-1了
bus.handlers[product] = bus.handlers[product][:handlerLen-1]
}
return nil
}
return fmt.Errorf("topic %s doesn't exist", product)
}
func (bus *EventBus) Publish(product string, args ...interface{}) {
bus.lock.Lock() // will unlock if handler is not found or always after setUpPublish
defer bus.lock.Unlock()
if handlers, ok := bus.handlers[product]; ok && 0 < len(handlers) {
for _, handler := range handlers {
//组装函数入参
funcType := handler.Type()
passedArguments := make([]reflect.Value, len(args))
for i, v := range args {
if v == nil {
// In 获取第i个入参的类型
// reflect.New和普通的new很像
// new是返回一个指向指定类型对象的指针
// reflect.New是返回指定类型反射对象的指针
// Elem获取反射对象对应的原始值对象,相当于解引用
// 否则对于func(a int, err error)返回的就是error*了
passedArguments[i] = reflect.New(funcType.In(i)).Elem()
} else {
passedArguments[i] = reflect.ValueOf(v)
}
}
handler.Call(passedArguments)
}
}
}
//test
func TestSub(t *testing.T) {
bus := NewBus()
if bus == nil {
t.Log("EventBus create fail!")
t.Fail()
}
//模拟三个用户订阅
flag := 0
fn := func() { flag += 1 }
bus.Subscribe("xiaomi", fn)
bus.Subscribe("xiaomi", fn)
bus.Subscribe("xiaomi", fn)
//xiaomi来了,开始回调函数通知链
bus.Publish("xiaomi")
if flag != 3 {
t.Fail()
}
//模拟用户逐一取消订阅
if bus.Unsubscribe("xiaomi", fn) != nil {
t.Fail()
}
if bus.Unsubscribe("xiaomi", fn) != nil {
t.Fail()
}
if bus.Unsubscribe("xiaomi", fn) != nil {
t.Fail()
}
//当三个用户都取消订阅后,再取消就会报错
if bus.Unsubscribe("xiaomi", fn) == nil {
t.Fail()
}
//验证入参是否传入正确
bus.Subscribe("topic", func(a int, err error) {
if a != 10 {
t.Fail()
}
if err != nil {
t.Fail()
}
})
bus.Publish("topic", 10, nil)
}
Reference
观察者模式及EventBus框架简单实现_GeorgiaStar的博客-CSDN博客_观察者模式框架
Go 语言陷阱 - 数组和切片 | Go 语言高性能编程 | 极客兔兔 (geektutu.com)
asaskevich/EventBus: [Go] Lightweight eventbus with async compatibility for Go (github.com)
GO反射(reflect)_小柏ぁ的博客-CSDN博客_go reflect
go的reflect_爬比我。的博客-CSDN博客_go reflect