设计模式中学习golang高级特性(二)

上篇文章介绍了两个设计模式,分别是单例模式和简单工厂模式,里面也引出了一些常用的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

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)

相关推荐
柏油1 小时前
MySQL InnoDB 行锁
数据库·后端·mysql
咖啡调调。1 小时前
使用Django框架表单
后端·python·django
白泽talk1 小时前
2个小时1w字| React & Golang 全栈微服务实战
前端·后端·微服务
摆烂工程师1 小时前
全网最详细的5分钟快速申请一个国际 “edu教育邮箱” 的保姆级教程!
前端·后端·程序员
一只叫煤球的猫1 小时前
你真的会用 return 吗?—— 11个值得借鉴的 return 写法
java·后端·代码规范
Asthenia04122 小时前
HTTP调用超时与重试问题分析
后端
颇有几分姿色2 小时前
Spring Boot 读取配置文件的几种方式
java·spring boot·后端
AntBlack2 小时前
别说了别说了 ,Trae 已经在不停优化迭代了
前端·人工智能·后端
@淡 定2 小时前
Spring Boot 的配置加载顺序
java·spring boot·后端