前言
宿舍每人 温度38℃+ 大寄
设计模式很重要,设计模式其实就是为了解决某一类问题而形成的代码写法,设计模式很多,但是并不是每个都很常用,我们只讲解─些常用的
设计模式分类大家可以参考: https://juejin.cn/post/6908528350986240014
go中最常用的设计模式是函数选项模式, grpc,kratos等等开源项目中比比皆是
有时候一个函数会有很多参数,为了方便函数的使用,我们会给希望给一些参数设定默认值,调用时只需要传与默认值不同的参数即可,类似于python里面的默认参数和字典参数,在java中可以提供多种构造函数,虽然 golang里面既没有默认参数也没有字典参数,但是我们有选项模式
注:函数选项模式是用来构造对象
go中没有构造器,要构造一个对象一般是直接实例化,或者采用NewXX的模式,其中
- 实例化然后复制属性的模式会写很多行代码,虽然可以直接采用A{name:xxx, b:xxx}的模式,但
是如果有个c属性的默认值不是空字符串咋办? - 所以整个过程使用实例化加属性设置的方式会让实例化很麻烦
- 使用new的方式也会有问题:如果可以一个参数设置,如果可以两个参数设置不得不使用
newA,newAandB等等各种组合设置 - 有没有办法可以同时解决上面的问题呢?-函数选项模式
选项模式的应用
从这里可以看到,为了实现选项的功能,我们增加了很多的代码,实现成本相对还是较高的,所以实践中需要根据自己的业务场景去权衡是否需要使用。个人总结满足下面条件可以考虑使用选项模式
- 参数确实比较复杂,影响调用方使用
- 参数确实有比较清晰明确的默认值
- 为参数的后续拓展考虑
在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 main
import "fmt"
type DbOptions struct {
Host string
Port int
Username string
Password string
DBName 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.Once
的Do
方法,并将初始化逻辑封装在一个匿名函数中。
b. 在匿名函数中创建DBPool
实例,并将其赋值给dbPoolIns
变量。
c. 返回dbPoolIns
变量。
使用sync.Once
实现的单例模式具有并发安全性,并且无需加锁即可实现懒加载,但需要创建匿名函数,代码稍微有些复杂。
2. 使用sync.Mutex和atomic实现单例模式
在下面的代码中,使用了sync.Mutex
和atomic
两个包来实现单例模式。具体实现步骤如下:
a. 在GetDBPool
函数中,首先判断initialized
变量是否为1,如果是,则直接返回dbPoolIns
变量。
b. 如果initialized
变量不是1,则获取锁,防止其他goroutine
同时执行初始化逻辑。
c. 在获取锁后,再次判断initialized
变量是否为0,如果是,则创建DBPool
实例,并将其赋值给dbPoolIns
变量,然后使用atomic.StoreUint32
函数将initialized
变量设置为1。
d. 释放锁并返回dbPoolIns
变量。
使用sync.Mutex
和atomic
实现的单例模式也具有并发安全性,并且代码比较简单易懂,但需要显式加锁,并且无法实现懒加载。
package main
import (
"sync"
"sync/atomic"
)
type DBPool struct {
Host string
Port int
UserName 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.Once
func 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 main
import "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 main
import "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) Book
Getpaper(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 chineseBookAssigner
fmt.Println(a.GetBook("语文书"))
fmt.Println(a.GetBook("数学书"))
}