> 文章列表 > 【设计模式】go语言中的 [函数选项,单例,工厂,责任链] 常用的设计模式

【设计模式】go语言中的 [函数选项,单例,工厂,责任链] 常用的设计模式

【设计模式】go语言中的 [函数选项,单例,工厂,责任链] 常用的设计模式

文章目录

  • 前言
  • 一、函数选项模式
  • 二、单例模式
  • 三、工厂模式
  • 四、责任链模式

前言

宿舍每人 温度38℃+ 大寄
设计模式很重要,设计模式其实就是为了解决某一类问题而形成的代码写法,设计模式很多,但是并不是每个都很常用,我们只讲解─些常用的

设计模式分类大家可以参考: https://juejin.cn/post/6908528350986240014

go中最常用的设计模式是函数选项模式, grpc,kratos等等开源项目中比比皆是

有时候一个函数会有很多参数,为了方便函数的使用,我们会给希望给一些参数设定默认值,调用时只需要传与默认值不同的参数即可,类似于python里面的默认参数和字典参数,在java中可以提供多种构造函数,虽然 golang里面既没有默认参数也没有字典参数,但是我们有选项模式

注:函数选项模式是用来构造对象

go中没有构造器,要构造一个对象一般是直接实例化,或者采用NewXX的模式,其中

  1. 实例化然后复制属性的模式会写很多行代码,虽然可以直接采用A{name:xxx, b:xxx}的模式,但
    是如果有个c属性的默认值不是空字符串咋办?
  2. 所以整个过程使用实例化加属性设置的方式会让实例化很麻烦
  3. 使用new的方式也会有问题:如果可以一个参数设置,如果可以两个参数设置不得不使用
    newA,newAandB等等各种组合设置
  4. 有没有办法可以同时解决上面的问题呢?-函数选项模式

选项模式的应用

从这里可以看到,为了实现选项的功能,我们增加了很多的代码,实现成本相对还是较高的,所以实践中需要根据自己的业务场景去权衡是否需要使用。个人总结满足下面条件可以考虑使用选项模式

  • 参数确实比较复杂,影响调用方使用
  • 参数确实有比较清晰明确的默认值
  • 为参数的后续拓展考虑

在golang 的很多开源项目里面也用到了选项模式,比如 grpc中的 rpc方法就是采用选项模式设计的,除了必填的rpc参数外,还可以一些选项参数,grpc_retry就是通过这个机制实现的,可以实现自动重试功能。


一、函数选项模式

函数选项模式(Functional Options Pattern):

函数选项模式是一种用于函数设计的模式,它允许函数接受可选参数,这些参数可以通过一种简单、灵活的方式进行设置,从而避免出现过多的函数重载。在Go语言中,函数选项模式通常使用可变参数列表结合函数类型参数的方式实现。通过该模式,我们可以更加灵活地控制函数的行为,并且可以方便地扩展函数的功能。

在这个示例代码中,DbOptions结构体类型定义了用于保存数据库连接选项的字段。为了实现函数选项模式,我们定义了一个名为Option的函数类型,用于设置DbOptions结构体中的字段值。WithHost函数是一个实现了Option函数类型的具体函数,用于设置数据库连接的主机地址。NewOpts函数接受任意数量的Option函数类型的参数,并将其应用于一个DbOptions结构体类型的实例上。

NewOpts函数中,我们首先定义一个带有默认值的DbOptions结构体类型的实例,并将其保存在dbopts变量中。接着,我们遍历所有传入的Option函数类型参数,逐个将其应用于dbopts变量中的字段。最后,我们返回dbopts变量的值,这是一个包含了所有设置过的选项的DbOptions结构体类型实例。

main函数中,我们调用NewOpts函数获取默认选项,并将其打印输出。此时,输出的结果中将包含我们在WithHost函数中设置的主机地址选项。这种方式可以使得我们在使用该函数时,只需要传递需要设置的选项,而不需要关心默认选项或者选项的顺序。

package mainimport "fmt"type DbOptions struct {Host     stringPort     intUsername stringPassword stringDBName   string
}type Option func(*DbOptions)// 这个函数主要用来设置Host
func WithHost(host string) Option {return func(o *DbOptions) {o.Host = host}
}func NewOpts(opts ...Option) DbOptions {//先实例化号dbOptions,填充上默认值dbopts := &DbOptions{Host:     "127.0.0.1",Port:     3306,Username: "root",Password: "123456",DBName:   "test",}for _, option := range opts {option(dbopts)}return *dbopts
}func main() {//NewDBClient(WithHost("192.168.0.1"))//opts := NewOpts(WithHost("192.168.0.1"))opts := NewOpts()fmt.Println(opts)//函数选项牧师大量引用了函数,
}

二、单例模式

单例模式(Singleton Pattern):

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。在Go语言中,单例模式通常使用包级别变量或者全局变量来实现,因为包级别变量只会被初始化一次,而全局变量则是唯一的。

1. 使用sync.Once实现单例模式
在下面的代码中,使用了sync.Once类型来确保在程序运行时只执行一次初始化逻辑,以创建唯一的DBPool实例。具体实现步骤如下:

a. 在GetDBPool2函数中,调用sync.OnceDo方法,并将初始化逻辑封装在一个匿名函数中。

b. 在匿名函数中创建DBPool实例,并将其赋值给dbPoolIns变量。

c. 返回dbPoolIns变量。

使用sync.Once实现的单例模式具有并发安全性,并且无需加锁即可实现懒加载,但需要创建匿名函数,代码稍微有些复杂。

2. 使用sync.Mutex和atomic实现单例模式
在下面的代码中,使用了sync.Mutexatomic两个包来实现单例模式。具体实现步骤如下:

a. 在GetDBPool函数中,首先判断initialized变量是否为1,如果是,则直接返回dbPoolIns变量。

b. 如果initialized变量不是1,则获取锁,防止其他goroutine同时执行初始化逻辑。

c. 在获取锁后,再次判断initialized变量是否为0,如果是,则创建DBPool实例,并将其赋值给dbPoolIns变量,然后使用atomic.StoreUint32函数将initialized变量设置为1

d. 释放锁并返回dbPoolIns变量。

使用sync.Mutexatomic实现的单例模式也具有并发安全性,并且代码比较简单易懂,但需要显式加锁,并且无法实现懒加载。

package mainimport ("sync""sync/atomic"
)type DBPool struct {Host     stringPort     intUserName string
}var dbPoolIns *DBPool
var lock sync.Mutex
var initialized uint32// 有问题的方法,并发
// 加锁 - 功能上没有问题,但是性能不好
// 高并发下,有bug
// goroutine1 进来,实例化dbPoolIns = &DBPool{}进想到一半,goroutine2进来,读到dbPoolIns != nil,返回dbPoolIns
func GetDBPool() *DBPool {if atomic.LoadUint32(&initialized) == 1 {return dbPoolIns}lock.Lock()defer lock.Unlock()if initialized == 0 {dbPoolIns = &DBPool{}//原子操作   一旦有一个线程执行到了atomic.LoadUint32(&initialized)   initialized就会被保护 确保initialized的原子性atomic.StoreUint32(&initialized, 1)}return dbPoolIns
}var once sync.Oncefunc GetDBPool2() *DBPool {once.Do(func() {dbPoolIns = &DBPool{}})return dbPoolIns
}
func main() {}

三、工厂模式

工厂模式(Factory Pattern):

工厂模式是一种创建型设计模式,它提供了一个抽象工厂接口来创建一系列相关的对象,而无需指定具体的类。在Go语言中,工厂模式通常使用接口和结构体的组合来实现,从而实现对不同对象的创建。通过该模式,我们可以更加灵活地创建对象,并且可以方便地扩展对象的种类。

在这个示例中,我们定义了一个名为“Book”的接口,该接口有一个名为“Name”的方法。然后,我们定义了三个具体的书籍类型:ChineseBook、MathBook和EnglishBook,并让它们都实现了“Book”接口的“Name”方法。

接下来,我们定义了一个名为“GetBook”的函数,该函数接受一个字符串参数“name”,并根据名称返回一个具体的书籍类型。我们使用一个简单的 switch 语句来实现这个功能,并在默认情况下返回 nil。

最后,在“main”函数中,我们调用“GetBook”函数,传递不同的书籍名称,并打印出返回的书籍类型。

工厂模式的好处是,在增加新的书籍类型时,我们只需要添加新的具体类型和一个对应的 case 语句,而不需要修改现有的代码。这使得我们的代码更加灵活和可扩展。

在实际应用中,工厂模式可以帮助我们更好地组织和管理代码,并将复杂的对象创建过程封装起来,使得我们的代码更加易于维护和扩展。

package mainimport "fmt"/*
在小明的学校,每一年开学都会发教材,
主要包括语文书、数学书、英语书,还有各种练习试卷。
这一天,小明去领了三本教材,分别是语文书、数学书和英语书,老师忙不过来,指定某个同学去发书,
同学们都去这个同学这里去领书。这个同学就是工厂。
*/
type Book interface {Name() string
}
type chineseBook struct {name string
}func (cb *chineseBook) Name() string {return cb.name
}type mathBook struct {name string
}func (mb *mathBook) Name() string {return mb.name
}type englishBook struct {name string
}func (eb *englishBook) Name() string {return eb.name
}
func GetBook(name string) Book {switch name {case "语文书":return &chineseBook{name: name}case "数学书":return &mathBook{name: name}case "英语书":return &englishBook{name: name}default:return nil}
}
func main() {fmt.Println(GetBook("语文书"))fmt.Println(GetBook("数学书"))
}

四、责任链模式

责任链模式(Chain of Responsibility Pattern):

责任链模式是一种行为型设计模式,它将一系列对象连接在一起,形成一个责任链。当请求被发送到该链上时,每个对象都有机会处理请求或将其传递给下一个对象,直到请求被处理为止。在Go语言中,责任链模式通常使用链表或者数组来实现,从而实现对请求的处理。通过该模式,我们可以更加灵活地处理请求,并且可以方便地扩展处理的对象。

在这个示例中,我们可以将 Assigner 接口看做一个处理器对象,用来处理请求并返回相应的书籍或文献。在 assigner 结构体中,我们实现了 GetBook 和 GetPaper 方法来获取不同类型的书籍或文献。在 chineseBookAssigner 结构体中,我们仅仅处理了语文书籍的请求,而其他类型的请求则会被忽略。这样,如果一个请求需要被处理,它就会被传递给第一个处理器对象,然后沿着处理链一直传递到最后一个处理器对象,直到找到能够处理请求的处理器对象或者到达处理链的末端。

在 main 函数中,我们创建了一个 chineseBookAssigner 对象并调用其 GetBook 方法来获取语文书籍的对象。由于 chineseBookAssigner 只处理语文书籍的请求,所以我们可以看到它成功地返回了一个 chineseBook 对象。而对于其他类型的请求,则会返回 nil。这样,我们就实现了一个简单的责任链模式。

package mainimport "fmt"/*
在小明的学校,每一年开学都会发教材,
主要包括语文书、数学书、英语书,还有各种练习试卷。
这一天,小明去领了三本教材,分别是语文书、数学书和英语书,老师忙不过来,指定某个同学去发书,
同学们都去这个同学这里去领书。这个同学就是工厂。
*/
type Book interface {Name() string
}
type Paper interface {Name() string
}type chineseBook struct {name string
}
type chinesePaper struct {name string
}func (cb *chineseBook) Name() string {return cb.name
}type mathBook struct {name string
}func (mb *mathBook) Name() string {return mb.name
}type englishBook struct {name string
}func (eb *englishBook) Name() string {return eb.name
}type Person struct{}type Assigner interface {GetBook(name string) BookGetpaper(string) Paper
}
type assigner struct{}func (a *assigner) GetBook(name string) Book {switch name {case "语文书":return &chineseBook{name: name}case "数学书":return &mathBook{name: name}case "英语书":return &englishBook{name: name}default:return nil}
}type chineseBookAssigner struct {
}func (cba *chineseBookAssigner) GetBook(name string) Book {if name == "语文书" {return &chineseBook{name: name}}return nil
}
func main() {var a chineseBookAssignerfmt.Println(a.GetBook("语文书"))fmt.Println(a.GetBook("数学书"))
}