Go 设计模式 · 2023年4月12日 0

《速通golang》4.设计模式中学习golang高级特性(二)【】

本文是全系列中第4 / 7篇:速通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,为处理同类型数据序列提供一个方便而高效的方式。

//切片的定义方式有两种
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("%vn%vn", nums1, nums2)
//nums1:[1 2 3 4 5]
//nums2:[]

//切片的四大操作

//1.深拷贝copy
copy(nums2, nums1)
fmt.Printf("nums1:%vnnums2:%vn", nums1, nums2)
nums1[0] = 3
fmt.Printf("nums1:%vnnums2:%vn", 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:%vnnums2:%vn", nums1, nums2)
nums1[0] = 3
fmt.Printf("nums1:%vnnums2:%vn", 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:%vnnums2:%vn", 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:%vnnums2:%vn", nums1, nums2)
fmt.Printf("cap:%vnlen:%vn", 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:%vnn", nums1)
//nums1:[1 3 4 5]


//5.Insert 某一个位置处新增
//还是得曲线救国,在某处连续的append
//三个点代表可变长度的参数,即代表append会追加多个元素,你要是不指定默认就追加一个的,但是你的参数又是一个切片
//所以编译会失败的,必须加...告诉编译器是变长的参数
nums1 = append(nums1[:1], append([]int{2}, nums1[1:]...)...)

fmt.Printf("nums1:%vnn", nums1)
//nums1:[1 2 2 3 4 5]

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

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

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用法非常方便,我们举个栗子:

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

    return ReadAll()
}

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

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{}) {

}

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

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)

本文是全系列中第4 / 7篇:速通golang
打赏 赞(0) 分享'
分享到...
微信
支付宝
微信二维码图片

微信扫描二维码打赏

支付宝二维码图片

支付宝扫描二维码打赏

文章目录