golang实现设计模式之单例模式总结-代码、优缺点、适用场景

发布时间 2023-05-30 08:23:43作者: 进击的davis

在日常业务开发中,适当应用设计模式,可以实现我们的业务需求,例如全局唯一配置,这里就需要用到单例模式。

什么情况下,我们可以使用单例模式呢?

这得考虑该模式的适用场景:

  • 用来控制类型实例的数量的,当需要确保一个类型只有一个实例

单例模式的适用场景:

  • 1.统计当前在线人数(网站计数器):用一个全局对象来记录。
  • 2.数据库连接池(控制资源):一般是采用单例模式,因为数据库连接是一种连接数据库资源,不易频繁创建和销毁,如缓存或者数据库等连接池对象。
  • 3.应用程序的日志(资源共享):一般日志内容是共享操作,需要在后面不断写入内容所以通常单例设计。
  • 4.应用配置:全局唯一实例。
  • 5.定义唯一属性:如特定的字符串。
  • 6.多线程的线程池设计。

单例模式优缺点

优点

  • 1.减少内存开销,尤其是频繁的创建和销毁实例。
  • 2.避免对资源对过多占用。

缺点

  • 1.没有抽象层,不能继承扩展很难。
  • 2.违背了“单一职责原则”,一个类只重视内部关系,而忽略外部关系。
  • 3.不适用于变化对象。
  • 4.滥用单例会出现一些负面问题:如连接池溢出,长时间不被使用,被GC。

单例模式的实现模式

在单例模式中,通常有懒汉模式和饿汉模式,区别在于是否程序启动时就创建实例。

饿汉模式

程序初始化时即创建,可能即使创建不一定使用,造成浪费,但可以及早暴露问题,定位问题。

该模式下的实现细节:

  • 1.单例类和构造方法不可导出,避免外部直接获取。
  • 2.全局变量中声明。
  • 3.初始化时,生成相关实例,并提供唯一外部接口获取实例。

代码实现:

package hungry

// 不可导出的变量、结构体、构造函数
type singletonHungry struct{}

var s *singletonHungry

func init() {
   s = &singletonHungry{}
}

// 唯一外部接口
func GetInstance() *singletonHungry {
   return s
}

懒汉模式

真正使用时再创建,需要注意并发安全问题,比如实例还未创建,有2个线程同时访问,这里就需要控制只会创建一个实例,通常来说可以通常加互斥锁实现,但也带来一定的性能问题,有时也需要通过 double check 解决。
该模式的实现细节:

  • 1.单例类和构造方法不可导出,避免外部直接获取。
  • 2.全局变量中声明。
  • 3.第一次创建时,通过GetInstance() xxx 创建实例。

比较优雅的 golang 实现中,我们可以通过 sync.Once 提供的接口实现,或者借助于 sync.Mutex + atomic.Load/atomic.Store 实现,二者实现是类似的,都能很好解决并发安全且只有一个实例的问题。

代码实现:

package lazy

import (
   "sync"
   "sync/atomic"
)

type singletonLazy struct{}

var s *singletonLazy

var once sync.Once

func GetInstance() *singletonLazy {
   once.Do(func() {
      s = &singletonLazy{}
   })

   return s
}

var done uint32
var mu sync.Mutex

func GetInstanceV2() *singletonLazy {
   // 原子操作,减少锁开销,类似于sync.Once的处理流程
   if atomic.LoadUint32(&done) == 1 {
      return s
   }

   mu.Lock()
   defer mu.Unlock()
   if atomic.LoadUint32(&done) == 0 {
      atomic.StoreUint32(&done, 1)
      s = &singletonLazy{}
   }

   return s
}

参考文档: