探索Golang高级特性:设计模式学习指南(二)

发表时间: 2024-03-12 10:54

上篇文章介绍了两个设计模式,分别是单例模式和简单工厂模式,里面也引出了一些常用的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,为处理同类型数据序列提供一个方便而高效的方式。

//切片的定义方式有两种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.深拷贝copycopy(nums2, nums1)fmt.Printf("nums1:%v\nnums2:%v\n", nums1, nums2)nums1[0] = 3fmt.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 = nums1fmt.Printf("nums1:%v\nnums2:%v\n", nums1, nums2)nums1[0] = 3fmt.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] = 0nums1 = 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设计模式, nums1[1:]...)...)fmt.Printf("nums1:%v\n\n", nums1)//nums1:[1 2 2 3 4 5]

好啦,有了这一系列的切片的操作秘籍,我们开始写观察者模式:

//codepackage observerimport "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反射,它可以帮助我们在函数运行时动态获取对象的类型和值,我们举个栗子:

package mainimport (	"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))	}}

第二,多用户是可以同时订阅一个事件的,这就意味着我们用链表存取用户通知的回调函数时,会有一个并发的考虑,那么我们改动这个链表的时就需要加锁,当处理完成后需要解锁,如果忘记解锁会直接死锁,对于Go语言有一个特别方便的关键字叫defer,字面意思是调用后延迟执行,一般用于释放资源和连接、关闭文件、释放锁等,这就和C++的析构函数很像。defer用法非常方便,我们举个栗子:

func ReadFile(filename string) ([]byte, error) {    //打开文件    f, err := os.Open(filename)    if err != nil {        return nil, err    }    //一会读完文件帮我关下灯,谢谢    defer f.close()     return ReadAll()}

下面开始正式编码,首先把架子搭出来:

package zhihueventbusimport (	"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{}) {}

接下来开始实现各个函数:

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)		}	}}//testfunc 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


mohuishou/go-design-pattern: golang design pattern go 设计模式实现,包含 23 种常见的设计模式实现,同时这也是极客时间-设计模式之美 的笔记 (github.com)

观察者模式及EventBus框架简单实现_GeorgiaStar的博客-CSDN博客_观察者模式框架

Go 语言陷阱 - 数组和切片 | Go 语言高性能编程 | 极客兔兔 (geektutu.com)

asaskevich/EventBus: [Go] Lightweight eventbus with async compatibility for Go (github.com)

SliceTricks · golang/go Wiki

Go Slice Tricks Cheat Sheet

深入挖掘分析Go代码 - 大海星 - 博客园

GO反射(reflect)_小柏ぁ的博客-CSDN博客_go reflect

go的reflect_爬比我。的博客-CSDN博客_go reflect

Go 延迟调用 defer 用法详解 - 腾讯云开发者社区-腾讯云 (tencent.com)

Golang的反射reflect深入理解和示例 - 简书 (jianshu.com)