前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【建议收藏】如何用Go写出优美的代码-Go的设计模式【单例模式,工厂方法模式】篇一

【建议收藏】如何用Go写出优美的代码-Go的设计模式【单例模式,工厂方法模式】篇一

作者头像
公众号-利志分享
发布2022-06-13 15:29:14
7110
发布2022-06-13 15:29:14
举报
文章被收录于专栏:利志分享利志分享

大家好,我是追麾(hui)。

接下来的几周时间给大家分享一系列Go设计模式文章,设计模式在我们的面试中也会被经常问到,像Java语言会用到设计模式,对于Go语言,设计模式使用会比较弱点,所以这里给大家一起来学习分享Go的设计模式,让我们在开发中也大量应用到设计模式,帮助我们的Go代码更加优美,可读性更好。

第一篇主要分享两种模式,单例模式和工厂方法模式。

Go的单例模式

单例模式定义:Ensure a class has only one instance, and provide a global point of access to it.(确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。)

单例模式优缺点

  • 优点
    • 减少内存开支,单例模式在内存中只有一个实例,特别是一个对象需要频繁的创建,销毁时,而且创建或者销毁时性能又无法优化,单例模式有非常明显的优势。
    • 减少系统性能的开销,当一个对象的产生需要多比较多的资源时,可以通过应用程序启动时直接产生一个单例对象,永久驻留内存。单例模式也可以在系统设置全局的访问点,优化和共享资源访问。
    • 避免对资源的多重占用,比如redis连接池对象,mysql连接池对象实现都可以避免同一个资源被同时操作。
  • 缺点
    • 代码扩展不方便,单例模式一般没有接口,扩展很困难,单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它要求“自行实例化”,并且提供单一实例、接口或抽象类是不可能被实例化的。
    • 单例模式与单一职责原则有冲突,一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境。
    • 对代码的可测性不好:如果是修改全局变量,测试的时候还要注意不同的测试用例对它的修改问题。
    • 会隐藏类之间的依赖关系:降低可读性,如果通过构造函数,参数传递等方式声明类之间的依赖关系,我们可以通过查看函数定义,就能容易识别出来。

单例模式的应用场景

  • 对目标实例使用是一致性的需求,即所有的客户端使用共享的实例,这样这个类就可以只有一个实例,并通过单例模式实现。例如一个软件配置信息封装在一个类中,这个类对所有客户端提供一致的配置信息,若所有客户端使用共享的目标类实例,就能保证该实例服务的一致性。
  • 对象实例化代价需求,当一个类的实例化需要付出昂贵的代价(指实例化所需的时间或资源)时,而该对象向所有客户端提供的又是无状态服务或者提供的服务与实例状态无关。减少目标类多次实例化的代价(即不需要每个客户端在使用时都进行目标类的对象实例化),可以起到优化程序的作用。

单例模式实现方式

懒汉式:使用的时候才进行初始化,即懒加载

  • 优点:只在第一次使用的时候调用,一定程度上节省了资源
  • 缺点:第一次加载需要进行及时实例化,反应稍慢,最大的问题是每次调用GetInstance都进行同步,会造成不少开销 我们通过Go示例来看下懒汉式的实现:
代码语言:javascript
复制
package main

func main() {
   // 调用实例对象
   GetInstance()
}

type Instance struct {
}

// 实例全局对象
var lazyInstance *Instance

func GetInstance() *Instance {
   // 判断第一次如果为空,则给lazy对象重新赋值。
   if lazyInstance == nil {
      lazyInstance = &Instance{}
   }
   return lazyInstance
}

在Go语言中,通过懒汉式来实现单例,重要点在判断第一次实例化的对象为空,则给对象重新赋值返回,这里如果每次调用GetInstance,出现并发问题,则会造成不少开销,像这种问题主要是使用双重检测来解决。

饿汉式:在程序初始化的时候或者类加载的时候就已经创建好对象,加载速度快。

  • 优点:实现简单,执行效率高,线程安全
  • 缺点:程序初始化或者类加载时就初始化实例,可能占用不必要内存浪费

我们通过Go示例来看下饿汉式实现:

代码语言:javascript
复制
package main

func main() {
 GetInstance()
}

//对象
type Instance struct {
}

var lazyInstance *Instance

// 饿汉式:在程序初始化的时候或者类加载的时候就已经创建好对象,加载速度快。
func init() {
 lazyInstance = &Instance{}
}

// 饿汉式调用
func GetInstance() *Instance {
 return lazyInstance
}

在Go语言中,通过饿汉式来实现单例主要依赖init函数初始化变量,或者自己定义函数(函数内部初始化变量)在初始化程序或者加载包之前初始化,然后定义一个通用的方法对外服务。

双重校验:在懒汉式的基础上,加上类级别的锁。在Go语言中主要是基于package做全局锁。

我们通过Go示例来看下双重校验实现:

代码语言:javascript
复制
package main

import "sync"

func main() {
 // 调用实例对象
 GetInstance()
}

type Instance struct {
}

// 实例全局对象
var lazyInstance *Instance

// 包级别的锁
var once = &sync.Once{}

func GetInstance() *Instance {
 // 判断第一次如果为空,则给lazy对象重新赋值。
 if lazyInstance == nil {
  // once.Do只执行一次
  once.Do(func() {
   lazyInstance = &Instance{}
  })
 }
 return lazyInstance
}

静态变量(Go里面通过常量来实现):Java中利用了静态内部类延迟初始化的特性,来达到与双重校验锁方式一样的功能,像Go中只能通过常量来实现,但是Go的常量仅支持整型,浮点型,字符串,bool值,所以在Go中实现常量实现单例模式没什么意义。

枚举类:该方式利用了枚举类的特性,不仅能避免线程同步问题,还防止反序列化重新创建新的对象。在Java中是能实现,但是Go中是没有这种,无法实现单例。

Go的工厂方法模式

工厂方法模式定义:工厂方法(Factory Method)类定义产品对象创建接口,但由子类实现具体产品对象的创建。

工厂方法模式优缺点

  • 优点
    • 良好的封装性,代码结构清晰:一个对象创建是有条件约束的,如一个调用者需要一个具体的产品对象,只要知道这个产品的类名(或约束字符串)就可以了,不用知道创建对象的艰辛过程,降低模块间的耦合。
    • 工厂方法模式的扩展性非常优秀:在增加产品类的情况下,只要适当地修改具体的工厂类或扩展一个工厂类,就可以完成扩展而拥抱变化。
    • 屏蔽产品类:产品类的实现如何变化,调用者都不需要关心,它只需要关心产品的接口,只要接口保持不变,系统中的上层模块就不发生变化,符合开闭原则。
    • 典型的解耦框架:高层模块只需要知道产品的抽象类,其他的实现类都不用关心,符合迪米特法则,我不需要的就不要去交流;也符合依赖倒置原则,只依赖产品类的抽象;当然也符合里氏替换原则,使用产品子类替换产品父类。
  • 缺点
    • 增加系统复杂性:当我们新增产品时,还需要对应工厂类,系统中类的个数会成倍增加,相当于系统复杂性。

工厂方法模式应用场景

  • 当业务类处理产品对象时,无法知道产品对象的具体类型,或不需要知道产品对象的具体类型(产品具有不同的子类)。
  • 当业务类处理不同的产品子类对象业务时,希望由自己的子类实现产品子类对象的创建。

工厂方法模式实现方式

在Go语言中,需要实现工厂方法模式,则需要使用到接口,定义一些公共的方法,用不同的struct对象来实现接口里面公共的方法,struct本身的对象的具体类型是不知道的,只有对象本身才知道。下面我们具体来看一个示例。

在很多聚合广告业务中,我们需要调用不同的广告厂商的接口,然后通过不同的广告厂商的数据存到自己的业务系统中,这个时候就可以使用工厂方法模式来实现,首先一些公共的方法RequestThirdApi(),每个厂商都要实现这些方法,每个厂商本身需要先按照自身的广告厂商接口组装请求数据,然后去请求,请求完了之后不同厂商接口返回的结构是不一样的,结果不一样,则可以通过处理成同样结构的数据到我们自己的系统。下面我们来看下具体实现代码:

代码语言:javascript
复制
package main

import "fmt"

func main() {
 NewFactory("kuaishou").RequestThirdApi()
}

// 做广告投放需要拉取不同的广告厂商,不同的广告厂商是不一样,但是都会存在请求第三方的方法
type ThirdResponseData struct {
}

// 工厂方法接口
type HttpRequestFactory interface {
 RequestThirdApi() *ThirdResponseData
}

// 头条厂商 工厂类
type TouTiaoFactory struct {
}

func (t *TouTiaoFactory) RequestThirdApi() *ThirdResponseData {
 // 组装请求
 // 执行请求
 // 处理请求响应结果,处理成ThirdResponseData这个的返回结构
 fmt.Println("头条")
 return &ThirdResponseData{}
}

// 快手厂商 工厂类
type KuaiShouFactory struct {
}

func (t *KuaiShouFactory) RequestThirdApi() *ThirdResponseData {
 // 组装请求
 // 执行请求
 // 处理请求响应结果,处理成ThirdResponseData这个的返回结构
 fmt.Println("快手")
 return &ThirdResponseData{}
}

// 用一个简单工厂封装工厂方法,这个是入口函数
func NewFactory(f string) HttpRequestFactory {
 switch f {
 case "kuaishou":
  return &KuaiShouFactory{}
 case "toutiao":
  return &TouTiaoFactory{}
 }
 return nil
}

上面只是一个简单的版本,实际比这个复杂,我们会处理组装请求,执行请求,响应都会用不同的方法来实现,这样会看起来比较清晰。

关于Go的设计模式第一篇,就先分享到这里。

参考文献

《软件设计模式之禅》

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-06-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 利志分享 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Go的单例模式
  • Go的工厂方法模式
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档