Singleton 单例模式
Design Patten
Go Design Patten 什么是单例模式
单例模式是指声明一个类并保证这个类只存在全局唯一的实例供外部使用
简单来讲,单例模式就是在全局视野下供外部使用的单个实例
应用场景
- 只允许存在一个实例的类,如作用于全局的统计或唯一标识生成器等
- 实例化时严重耗费系统资源的类,如池化(连接池、协程池)、与第三方进行交互的客户端
- 入参复杂的系统模块组件,如 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 singletonvar s *singletontype 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 singletonvar (s *singletonmux 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 singletonvar (s *singletonmux 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 singletonimport "sync"// 声明单例与互斥锁var (s *singletonmux 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的实现思路了// Do calls the function f if and only if Do is being called for the// first time for this instance of Once. In other words, given//// var once Once//// if once.Do(f) is called multiple times, only the first call will invoke f,// even if f has a different value in each invocation. A new instance of// Once is required for each function to execute.//// Do is intended for initialization that must be run exactly once. Since f// is niladic, it may be necessary to use a function literal to capture the// arguments to a function to be invoked by Do://// config.once.Do(func() { config.init(filename) })//// Because no call to Do returns until the one call to f returns, if f causes// Do to be called, it will deadlock.//// If f panics, Do considers it to have returned; future calls of Do return// without calling f.func (o *Once) Do(f func()) {// Note: Here is an incorrect implementation of Do://// if o.done.CompareAndSwap(0, 1) {// f()// }//// Do guarantees that when it returns, f has finished.// This implementation would not implement that guarantee:// given two simultaneous calls, the winner of the cas would// call f, and the second would return immediately, without// waiting for the first's call to f to complete.// This is why the slow path falls back to a mutex, and why// the o.done.Store must be delayed until after f returns.if o.done.Load() == 0 {// Outlined slow-path to allow inlining of the fast-path.o.doSlow(f)}}func (o *Once) doSlow(f func()) {o.m.Lock()defer o.m.Unlock()if o.done.Load() == 0 {defer o.done.Store(1)f()}}
sync.once也是基于double check机制实现对全局任务单次执行的保证- 检查
Once.Done的值是否为 0,由于此处的Done采用的是atomic.uint32, 因此在这里避免了繁重的加锁动作 - 如果不为 0 则证明该任务已被执行
- 如果为 0,则进行加锁操作
- 对
Once.Done的值进行第二次检测,确保在竞争锁期间Once.Done的值为发生改变,即该任务未执行 - 执行任务并解锁
- 检查
对比
- 饿汉式在程序启动时便会对单例进行初始化,如果单例对象迟迟不被使用,甚至永远不被使用,那初始化过程可能会是一次无用的性能损耗
- 懒汉式在单例首次被使用时才会被初始化,但如果初始化工作中存在异常,则会导致程序崩溃,因此,如果可能导致程序奔溃或者存在异常的单例(e.g.数据库连接池),应该在代码编译运行之初就提前暴露,因此更适合采用饿汉式单例模式,从而更有利于问题定位与解决