Go 到底有没有单例模式呢?一起来 try 一下吧~
Singleton 单例模式
什么是单例模式
单例模式是指声明一个类并保证这个类只存在全局唯一的实例供外部使用
简单来讲,单例模式就是在全局视野下供外部使用的单个实例
应用场景
- 只允许存在一个实例的类,如作用于全局的统计或唯一标识生成器等
- 实例化时严重耗费系统资源的类,如池化(连接池、协程池)、与第三方进行交互的客户端
- 入参复杂的系统模块组件,如 MVC 架构下的 controller、service、dao 等
实现模式
饿汉式
应用启动即完成单例的初始化
实现流程
- 单例类和构造方法声明为不可导出类型,避免外部直接获取(避免外界直接初始化类实例)
- 在程序启动时,初始化好一个全局单一的实例
- 暴露一个可导出的获取实例的方法,用于返回当前单例对象
// 饿汉式单例模式实现
package singleton
// 定义导出方法
type Singleton interface{
Work()
}
// 定义实现类
type singleton struct{}
// 声明单例
var s *singleton
// 实现单例方法
func (s *singleton) Work() {}
// 初始化单例
func init() {
s = newSingleton()
}
// 构造方法
func newSingleton() *singleton {
return &singleton{}
}
// 导出获取单例方法
func GetSingleton() Singleton {
return s
}
懒汉式
被使用到时再执行初始化
实现流程
- 单例类和构造方法声明为不可导出类型,避免外部直接获取(避免外界直接初始化类实例)
- 声明一个全局单一的实例,但不进行初始化
- 暴露一个可导出的获取实例的方法,用于返回当前单例对象
- 在获取实例的方法被调用时,判断单例是否初始化,选择是否初始化与返回单例
// 懒汉式单例模式实现
package singleton
import "sync"
// 声明单例与互斥锁
var (
s *singleton
once sync.Once
)
// 定义导出方法
type Singleton interface {
Work()
}
// 定义实现类
type singleton struct{}
// 实现方法
func (s *singleton) Work() {}
// 构造函数
func newSingleton() *singleton {
return &singleton{}
}
// 导出获取单例方法
func GetSingleton() Singleton {
once.Do(func() {
s = newSingleton()
})
return s
}
-
饿汉式单例模式的演进之路
// 懒汉式单例模式实现 package singleton var s *singleton type Singleton interface{ Work() } type singleton struct{} func (s *singleton) Work() {} func newSingleton() *singleton { return &singleton{} } func GetSingleton() Singleton { if s == nil { s = newSingleton() } return s }
如果系统中存在并发调用,那 singleton 便有可能被初始化多次,违背了全剧唯一的原则,因此我们再次基础上进行加锁,得到了以下代码
// 懒汉式单例模式实现 package singleton var ( s *singleton mux sync.Mutex ) type Singleton interface{ Work() } type singleton struct{} func (s *singleton) Work() {} func newSingleton() *singleton { return &singleton{} } func GetSingleton() Singleton { mux.Lock() defer mux.Unlock() if s == nil { s = newSingleton() } return s }
那我们这样实现总没有问题了吧?其实是有的,这样实现确实是避免了并发导致初始化多个实例的情况,但是在每次获取单例时,都会进行加锁操作,会造成无意义的性能浪费
// 懒汉式单例模式实现 package singleton var ( s *singleton mux sync.Mutex ) type Singleton interface{ Work() } type singleton struct{} func (s *singleton) Work() {} func newSingleton() *singleton { return &singleton{} } func GetSingleton() Singleton { if s != nil { return s } mux.Lock() defer mux.Unlock() s = newSingleton() return s }
那这样我们便解决了每次获取单例时都需要加锁的情况了,但是当前代码好像又引入了新的并发安全问题
- 当没有实例化对象时,并发获取单例会引起锁竞争,当获取到锁的协程初始化单例后,释放锁,处于阻塞等待状态的另一个协程获取到锁后,会重新进行初始化,造成多个实例
因此我们引入了 double check 的机制来解决这个问题,代码如下
// 懒汉式单例模式实现 package singleton import "sync" // 声明单例与互斥锁 var ( s *singleton mux sync.Mutex ) // 定义导出方法 type Singleton interface { Work() } // 定义实现类 type singleton struct{} // 实现方法 func (s *singleton) Work() {} // 构造函数 func newSingleton() *singleton { return &singleton{} } // 导出获取单例方法 func GetSingleton() Singleton { // 如果单例完成初始化,返回单例 if s != nil { return s } // 加锁保证只有一个初始化单例任务在执行 mux.Lock() defer mux.Unlock // double check // 防止等待线程获取锁后再次初始化单例 if s != nil { return s } // 初始化单例 s = newSingleton() // 返回单例 return s }
-
而 double check 的实现思路就已经非常接近
sync.Once
的实现思路了
对比
- 饿汉式在程序启动时便会对单例进行初始化,如果单例对象迟迟不被使用,甚至永远不被使用,那初始化过程可能会是一次无用的性能损耗
- 懒汉式在单例首次被使用时才会被初始化,但如果初始化工作中存在异常,则会导致程序崩溃,因此,如果可能导致程序崩溃或者存在异常的单例(e.g.数据库连接池),应该在代码编译运行之初就提前暴露,因此更适合采用饿汉式单例模式,从而更有利于问题定位与解决
转载自:https://juejin.cn/post/7389089457733599269