介绍
单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点。
因为它同时解决了两个问题,所以它违反了单一职责原则。
使用场景
什么场景适合使用单例模式呢?
某个类对于所有客户端只有一个可用的实例
比如记录应用程序的运行日志,因为记录日志的文件只有一个,所以只能有一个日志类的实例向日志文件中写入,否则会出现日志内容互相覆盖的问题。
需要更加严格地控制全局变量
所谓更加严格地控制全局变量,即使用单例模式确保一个类只有一个实例,除了该类自己以外,无法通过任何方式替换缓存的实例(控制全局变量)。
实现方式
在 Go 语言中,没有类 Class
的概念,我们可以使用结构体 struct
替代。
- 定义一个私有变量,用于保存单例类的实例。
- 定义一个公有函数,用于获取单例类的实例。
- 在公有函数中实现 “延迟实例化”。
04
Go 实现
实现单例模式,一般分为三种方式,分别是急切实例化(饿汉式)、延迟实例化(懒汉式)和双重检查加锁实例化。
此外,Go 标准库 sync/once
,也可用于实现单例模式。
急切实例化:
急切实例化(饿汉式)是指在导入包时自动创建实例,并且创建的实例会一直存储在内存中,它是并发安全的,可以使用 init()
初始化函数实现。
一般用于实例占用资源少,并且使用频率高的场景。
type singletonV1 struct { } var instance *singletonV1 func init() { instance = new(singletonV1) fmt.Printf("%p\n", instance) } func GetInstance() *singletonV1 { return instance }
延迟实例化:
延迟实例化(懒汉式)是指在导入包时不自动创建实例,而是在初次使用时,才会创建实例。它不是并发安全的,可以通过加锁确保协程的并发安全,但是会影响程序的性能。
一般用于实例占用资源多,并且使用率低的场景。
非并发安全:
type singletonV2 struct { } var instance *singletonV2 func GetInstance() *singletonV2 { if instance == nil { instance = new(singletonV2) } fmt.Printf("%p\n", instance) return instance }
并发安全:
type singletonV3 struct { } var instance *singletonV3 var mu sync.Mutex func GetInstance() *singletonV3 { if instance == nil { mu.Lock() defer mu.Unlock() instance = new(singletonV3) } fmt.Printf("%p\n", instance) return instance }
双重检查加锁实例化:
双重检查加锁实例化实际上是对通过锁支持并发的延迟实例化的优化,减少锁操作,降低性能损耗。
type singletonV4 struct { } var instance *singletonV4 var mu sync.Mutex func GetInstance() *singletonV4 { if instance == nil { mu.Lock() defer mu.Unlock() if instance == nil { instance = new(singletonV4) } } fmt.Printf("%p\n", instance) return instance }
阅读上面这段代码,第一次 nil
判断,是为了减少锁操作,第二次 nil
判断,是为了确保只有一个争抢到锁的协程创建实例。
双重检查加锁实例化(原子操作):
双重检查加锁实例化的两次检查,我们还可以将第一次 nil
判断,改为通过使用 sync/atomic
包的原子操作判断,决定是否需要进行争抢锁。
type singletonV5 struct { } var instance *singletonV5 var mu sync.Mutex var done uint32 func GetInstance() *singletonV5 { if atomic.LoadUint32(&done) == 0 { mu.Lock() defer mu.Unlock() if instance == nil { defer atomic.StoreUint32(&done, 1) instance = new(singletonV5) } } fmt.Printf("%p\n", instance) return instance }
sync/once:
我们在介绍 Go 语言并发的文章中,了解到 sync/once
包的 Do()
方法可以确保只执行一次。
type singletonV6 struct { } var instance *singletonV6 var once sync.Once func GetInstance() *singletonV6 { once.Do(func() { instance = new(singletonV6) }) fmt.Printf("%p\n", instance) return instance }
我们可以通过阅读 sync/once
包的源码发现,实际上 Do()
方法也是使用了 sync/atomic
包的 StoreUint32
方法和 LoadUint32()
方法。
05
总结
本文我们介绍了创建型设计模式-单例模式,并且介绍了几种 Go 实现方式。
需要注意的是,我们在高并发场景中,需要考虑并发安全的问题。
推荐阅读:
参考资料: