设计模式中学习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)

相关推荐
开心工作室_kaic1 小时前
springboot485基于springboot的宠物健康顾问系统(论文+源码)_kaic
spring boot·后端·宠物
0zxm1 小时前
08 Django - Django媒体文件&静态文件&文件上传
数据库·后端·python·django·sqlite
刘大辉在路上9 小时前
突发!!!GitLab停止为中国大陆、港澳地区提供服务,60天内需迁移账号否则将被删除
git·后端·gitlab·版本管理·源代码管理
追逐时光者10 小时前
免费、简单、直观的数据库设计工具和 SQL 生成器
后端·mysql
初晴~11 小时前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
盖世英雄酱5813611 小时前
InnoDB 的页分裂和页合并
数据库·后端
小_太_阳11 小时前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
直裾11 小时前
scala借阅图书保存记录(三)
开发语言·后端·scala
星就前端叭12 小时前
【开源】一款基于Vue3 + WebRTC + Node + SRS + FFmpeg搭建的直播间项目
前端·后端·开源·webrtc