编辑
2025-02-28
技术学习
00

目录

golang 设计模式
1 设计模式概述
2 创建型模式
2.1 单例模式
2.1.1 饿汉式
2.1.2 懒汉式
2.1.3 并发安全
2.2 工厂模式
2.2.1 简单工厂模式
2.2.2 工厂方法模式
2.2.3 抽象工厂模式
3 结构型模式
3.1 策略模式
3.2 模板模式
4 行为型模式
4.1 代理模式
4.2 选项模式
5 总结

golang 设计模式

Tags: Golang

Published: 2024年6月19日

概要: 本文介绍了Go语言中的设计模式,包括创建型模式、结构型模式和行为型模式,并提供了各种模式的示例代码和解析。

1 设计模式概述

设计模式是啥呢?简单来说,就是将软件开发中需要重复性解决的编码场景,按最佳实践的方式抽象成一个模型,模型描述的解决方法就是设计模式。使用设计模式,可以使代码更易于理解,保证代码的重用性和可靠性。

Untitled.png

2 创建型模式

创建型模式(Creational Patterns),它提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。

这种类型的设计模式里,单例模式和工厂模式(具体包括简单工厂模式、抽象工厂模式和工厂方法模式三种)在Go项目开发中比较常用。

2.1 单例模式

单例模式的目的是为了保证一个类仅有一个实例,并提供一个访问它的全局访问点.

单例模式的两种表现形式。

  • 饿汉式:类加载时,就进行实例化。
  • 懒汉式,第一次引用类时才进行实例化。

2.1.1 饿汉式

单例对象是在包加载时立即被创建,所以这个方式叫作饿汉式

singleton 包在被导入时会自动初始化 instance 实例,使用时通过调用 singleton.GetSingleton() 函数即可获得 singleton 这个结构体的单例对象。

package singleton type singleton struct{} var instance = &singleton{} func GetSingleton() *singleton {   return instance }

2.1.2 懒汉式

懒汉式模式下实例只有在第一次被使用时才被创建。

package singleton type singleton struct{} var instance *singleton func GetSingleton() *singleton {   if instance == nil {       instance = &singleton{}   }   return instance }

2.1.3 并发安全

懒汉式单例模式并发安全问题,比如在创建实例时,如果多个线程同时进行,则会出现并发问题。

为了解决并发往问题,可以使用以下三种方法:

  • 使用锁

  • 使用sync/atomic包,原子化加载并设置一个标志,表明是否已初始化实例

  • 使用sync.Once

    package singleton import "sync" type singleton struct{} var instance *singleton var once sync.Once func GetSingleton() *singleton {   once.Do(func() {       instance = &singleton{}   })   return instance }

Once 是一个结构体,在执行 Do 方法的内部通过 atomic 操作和加锁机制来保证并发安全,且 once.Do 能够保证多个 goroutine 同时执行时 &singleton{} 只被创建一次。

2.2 工厂模式

工厂模式是创建型模式之一。由于创建类的代码和使用类的代码分离,符合单一职责和开闭原则,代码扩展性高.

按实际业务场景划分,工厂模式有 3 种不同的实现方式,俗称工厂三兄弟。

  • 简单工厂模式
  • 工厂方法模式
  • 抽象工厂模式

2.2.1 简单工厂模式

简单工厂模式(Simple Factory Pattern)又称为静态工厂方法(Static Factory Method)模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。

优点

  • 创建类的代码和使用类的代码分离,符合单一职责和开闭原则,代码扩展性高。
  • 客户端无需知道所创建具体产品的类名,只需知道参数即可。
  • 也可以引入配置文件,在不修改客户端代码的情况下更换和添加新的具体产品类。

缺点

  • 简单工厂模式的工厂类单一,职责过重,一旦异常,整个系统将受影响。且违背开闭原则。
  • 使用简单工厂模式会增加系统中类的个数(引入新的工厂类),增加系统的复杂度和理解难度。
  • 系统扩展困难,一旦增加新产品不得不修改工厂逻辑,在产品类型较多时,可能造成逻辑过于复杂。

2.2.2 工厂方法模式

工厂方法模式(Factory method pattern)是一种实现了工厂概念的面向对象设计模式。工厂方法模式定义一个用于创建对象的接口,但让实现这个接口的子类来决定实例化哪个类。工厂方法让类的实例化延迟到子类中进行。

从工厂方法模式来看,工厂类不再单一,我们解耦了对产品的操作,每种操作都作为一种工厂类,做到了工厂隔离,后续如果需要新的产品,则只需要根据抽象的类来定义产品即可,再设计新的工厂生产产品,不会改动之前的任何代码,符合开闭原则思想。

优点

  • 不需要记住具体类名,甚至连具体参数都不用记忆。
  • 系统的可扩展性也就变得非常好,无需修改接口和原类。
  • 符合开闭原则。

缺点

  • 增加系统中类的个数,复杂度和理解度增加。
  • 增加了系统的抽象性和理解难度。

2.2.3 抽象工厂模式

抽象工厂模式(Abstract factory pattern)引出了产品族和产品等级结构概念,其目的是为了更加高效的生产同一个产品组产品。

产品等级结构: 产品的等级结构就是产品的继承结构,如一个模型工厂,可以画出圆形,长方形和正方形的模型。这里抽象的模型工厂和具体的模型构成了产品等级结构。 产品族: 在抽象工厂模式中,产品族指的是同一个工厂生产的,位于不同产品等级结构的一组产品。如模具厂生产的红色圆形模具,圆形模型属于模型产品等级结构中,红色属于颜料产品等级结构中。

Untitled 1.png

优点

  • 拥有工厂方法模式的优点。
  • 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。
  • 增加新的产品族很方便,无须修改已有系统,符合开闭原则。

缺点

  • 增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了开闭原则。

3 结构型模式

特点是关注类和对象的组合

3.1 策略模式

策略模式(Strategy Pattern)定义一组算法,将每个算法都封装起来,并且使它们之间可以互换。

在什么时候,我们需要用到策略模式呢?

在项目开发中,我们经常要根据不同的场景,采取不同的措施,也就是不同的策略。比如,假设我们需要对a、b 这两个整数进行计算,根据条件的不同,需要执行不同的计算方式。我们可以把所有的操作都封装在同一个函数中,然后通过 if ... else ... 的形式来调用不同的计算方式,这种方式称之为硬编码

在实际应用中,随着功能和体验的不断增长,我们需要经常添加/修改策略,这样就需要不断修改已有代码,不仅会让这个函数越来越难维护,还可能因为修改带来一些bug。所以为了解耦,需要使用策略模式,定义一些独立的类来封装不同的算法,每一个类封装一个具体的算法(即策略)。

package strategy // 策略模式 // 定义一个策略类 type IStrategy interface { do(int, int) int } // 策略实现:加 type add struct{} func (*add) do(a, b int) int { return a + b } // 策略实现:减 type reduce struct{} func (*reduce) do(a, b int) int { return a - b } // 具体策略的执行者 type Operator struct { strategy IStrategy } // 设置策略 func (operator *Operator) setStrategy(strategy IStrategy) { operator.strategy = strategy } // 调用策略中的方法 func (operator *Operator) calculate(a, b int) int { return operator.strategy.do(a, b) }

在上述代码中,我们定义了策略接口 IStrategy,还定义了 add 和 reduce 两种策略。最后定义了一个策略执行者,可以设置不同的策略,并执行,例如:

func TestStrategy(t *testing.T) { operator := Operator{} operator.setStrategy(&add{}) result := operator.calculate(1, 2) fmt.Println("add:", result) operator.setStrategy(&reduce{}) result = operator.calculate(2, 1) fmt.Println("reduce:", result) }

3.2 模板模式

模板模式 (Template Pattern)定义一个操作中算法的骨架,而将一些步骤延迟到子类中。这种方法让子类在不改变一个算法结构的情况下,就能重新定义该算法的某些特定步骤。

简单来说,模板模式就是将一个类中能够公共使用的方法放置在抽象类中实现,将不能公共使用的方法作为抽象方法,强制子类去实现,这样就做到了将一个类作为一个模板,让开发者去填充需要填充的地方。

package template import "fmt" type Cooker interface { fire() cooke() outfire() } // 类似于一个抽象类 type CookMenu struct { } func (CookMenu) fire() { fmt.Println("开火") } // 做菜,交给具体的子类实现 func (CookMenu) cooke() { } func (CookMenu) outfire() { fmt.Println("关火") } // 封装具体步骤 func doCook(cook Cooker) { cook.fire() cook.cooke() cook.outfire() } type XiHongShi struct { CookMenu } func (*XiHongShi) cooke() { fmt.Println("做西红柿") } type ChaoJiDan struct { CookMenu } func (ChaoJiDan) cooke() { fmt.Println("做炒鸡蛋") }
func TestTemplate(t *testing.T) { // 做西红柿 xihongshi := &XiHongShi{} doCook(xihongshi) fmt.Println("\n=====> 做另外一道菜") // 做炒鸡蛋 chaojidan := &ChaoJiDan{} doCook(chaojidan) }

4 行为型模式

特点是关注对象之间的通信

4.1 代理模式

代理模式 (Proxy Pattern),可以为另一个对象提供一个替身或者占位符,以控制对这个对象的访问。

package proxy import "fmt" type Seller interface { sell(name string) } // 火车站 type Station struct { stock int //库存 } func (station *Station) sell(name string) { if station.stock > 0 {   station.stock--   fmt.Printf("代理点中:%s买了一张票,剩余:%d \n", name, station.stock) } else {   fmt.Println("票已售空") } } // 火车代理点 type StationProxy struct { station *Station // 持有一个火车站对象 } func (proxy *StationProxy) sell(name string) { if proxy.station.stock > 0 {   proxy.station.stock--   fmt.Printf("代理点中:%s买了一张票,剩余:%d \n", name, proxy.station.stock) } else {   fmt.Println("票已售空") } }

上述代码中,StationProxy代理了Station,代理类中持有被代理类对象,并且和被代理类对象实现了同一接口。

4.2 选项模式

在Python语言中,创建一个对象时,可以给参数设置默认值,这样在不传入任何参数时,可以返回携带默认值的对象,并在需要时修改对象的属性。这种特性可以大大简化开发者创建一个对象的成本,尤其是在对象拥有众多属性时。

而在Go语言中,因为不支持给参数设置默认值,为了既能够创建带默认值的实例,又能够创建自定义参数的实例,不少开发者会通过以下两种方法来实现:

第一种方法,我们要分别开发两个用来创建实例的函数,一个可以创建带默认值的实例,一个可以定制化创建实例。

使用这种方式,创建同一个Connection实例,却要实现两个不同的函数,实现方式很不优雅。

另外一种方法相对优雅些。我们需要创建一个带默认值的选项,并用该选项创建实例:

package options import ( "time" ) const ( defaultTimeout = 10 defaultCaching = false ) type Connection struct { addr   string cache   bool timeout time.Duration } type ConnectionOptions struct { Caching bool Timeout time.Duration } func NewDefaultOptions() *ConnectionOptions { return &ConnectionOptions{   Caching: defaultCaching,   Timeout: defaultTimeout, } } // NewConnect creates a connection with options. func NewConnect(addr string, opts *ConnectionOptions) (*Connection, error) { return &Connection{   addr:   addr,   cache:   opts.Caching,   timeout: opts.Timeout, }, nil }

使用这种方式,虽然只需要实现一个函数来创建实例,但是也有缺点:为了创建Connection实例,每次我们都要创建ConnectionOptions,操作起来比较麻烦。

那么有没有更优雅的解决方法呢?答案当然是有的,就是使用选项模式来创建实例。以下代码通过选项模式实现上述功能:

package options import ( "time" ) type Connection struct { addr   string cache   bool timeout time.Duration } const ( defaultTimeout = 10 defaultCaching = false ) type options struct { timeout time.Duration caching bool } // Option overrides behavior of Connect. type Option interface { apply(*options) } type optionFunc func(*options) func (f optionFunc) apply(o *options) { f(o) } func WithTimeout(t time.Duration) Option { return optionFunc(func(o *options) {   o.timeout = t }) } func WithCaching(cache bool) Option { return optionFunc(func(o *options) {   o.caching = cache }) } // Connect creates a connection. func NewConnect(addr string, opts ...Option) (*Connection, error) { options := options{   timeout: defaultTimeout,   caching: defaultCaching, } for _, o := range opts {   o.apply(&options) } return &Connection{   addr:   addr,   cache:   options.caching,   timeout: options.timeout, }, nil }

在上面的代码中,首先我们定义了options结构体,它携带了timeout、caching两个属性。接下来,我们通过NewConnect创建了一个连接,NewConnect函数中先创建了一个带有默认值的options结构体变量,并通过调用

for _, o := range opts {

o.apply(&options)

}

来修改所创建的options结构体变量。

需要修改的属性,是在NewConnect时,通过Option类型的选项参数传递进来的。可以通过WithXXX函数来创建Option类型的选项参数:WithTimeout、WithCaching。

Option类型的选项参数需要实现apply(*options)函数,结合WithTimeout、WithCaching函数的返回值和optionFunc的apply方法实现,可以知道o.apply(&options)其实就是把WithTimeout、WithCaching传入的参数赋值给options结构体变量,以此动态地设置options结构体变量的属性。

这里还有一个好处:我们可以在apply函数中自定义赋值逻辑,例如o.timeout = 100 * t。通过这种方式,我们会有更大的灵活性来设置结构体的属性。

选项模式有很多优点,例如:支持传递多个参数,并且在参数发生变化时保持兼容性;支持任意顺序传递参数;支持默认值;方便扩展;通过WithXXX的函数命名,可以使参数意义更加明确,等等。

不过,为了实现选项模式,我们增加了很多代码,所以在开发中,要根据实际场景选择是否使用选项模式。选项模式通常适用于以下场景:

  • 结构体参数很多,创建结构体时,我们期望创建一个携带默认值的结构体变量,并选择性修改其中一些参数的值。
  • 结构体参数经常变动,变动时我们又不想修改创建实例的函数。例如:结构体新增一个retry参数,但是又不想在NewConnect入参列表中添加retry int这样的参数声明。

如果结构体参数比较少,可以慎重考虑要不要采用选项模式。

5 总结

Untitled 2.png

本文作者:AstralDex

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!