我们之前已经看过Golang常用设计模式中的装饰模式和选项模式,今天我们就来看看Golang设计模式中最简单的单例模式。单例模式的作用是保证无论对象被实例化多少次,全局只存在一个实例。根据这个特性,我们可以将其应用于全局唯一的配置、数据库连接对象、文件访问对象等。Go语言中单例模式的实现方式有很多种,下面我们一起来看看。饿了么中国式的单例模式实现起来非常简单。直接看代码:packagesingletontypesingletonstruct{}varinstance=&singleton{}funcGetSingleton()*singleton{returninstance}singleton包在导入时会自动初始化instance实例,单例结构体的singleton对象可以使用时调用singleton.GetSingleton()函数获取。该方法的单例对象是在加载包时立即创建的,所以该方法被称为饿了么风格。另一种对应的实现方式称为惰性模式。在惰性模式下,实例会在第一次使用时被创建。需要注意的是,饿了么中文实现单例模式的方式虽然简单,但大多数情况下不推荐使用。因为如果在实例化单例的时候初始化内容太多,加载程序的时间会很长。惰性风格接下来我们看看如何通过惰性风格来实现单例模式:hungry风格的实现,lazy风格将实例化单例结构的代码移到了GetSingleton()函数中。这将对象实例化推迟到第一次调用GetSingleton()时。但是通过instance==nil的判断来实现单例并不是很靠谱。如果多个goroutine同时调用GetSingleton(),并发安全得不到保障。支持并发的单例如果你写过Go语言的并发编程,你应该很快想到如何解决惰性单例模式的并发安全问题,比如下面这样:packagesingletonimport"sync"typesingletonstruct{}varinstance*singletonvarmusync.MutexfuncGetSingleton()*singleton{mu.Lock()defermu.Unlock()ifinstance==nil{instance=&singleton{}}returninstance}上面的代码是通过锁机制修改的,即在GetSingleton()函数的开头添加如下两行代码:mu.Lock()defermu.Unlock()锁机制可以有效保证实现单例模式的函数并发安全。但是锁机制的使用也带来了一些问题,使得程序在每次调用GetSingleton()时都要执行加锁和解锁步骤,导致程序性能下降。双锁加锁会导致程序性能下降,但是没有锁,程序的并发安全就得不到保证。为了解决这个问题,有人提出了Double-CheckLocking的解决方案:Lock()defermu.Unlock()ifinstance==nil{instance=&singleton{}}}returninstance}从上面可以看出,所谓的双重加锁其实就是在程序前加一层instance=被锁住了。=nil判断,这种方式兼顾了性能和安全性。然而,这使得代码看起来有点奇怪。外层已经判断了instance==nil,但是锁上锁后,第二次判断instance==nil。其实外层的instance==nil判断是为了提高程序的执行效率,避免原来每次调用GetSingleton()时都进行加锁操作,加锁的粒度更加细化。简单的说,如果实例已经存在,则不需要进入if逻辑,程序直接返回实例即可。内层的instance==nil判断考虑了并发安全。考虑到极端情况下,多个goroutine同时到达加锁这一步,内部判断会在这里发挥作用。Gopher惯用的方案虽然兼顾了性能和并发安全,但代码明显丑陋,不符合广大Gopher的期望。好在Go语言在sync包中提供了Once机制,帮助我们写出更优雅的代码:(){instance=&singleton{}})returninstance}Once是一个结构体,Do方法执行内部通过原子操作和锁机制保证并发安全,once.Do可以保证多个goroutine同时执行&singleton{}仅创建一次。其实,Once并不神秘。它的内部实现和上面使用的双重锁机制非常相似,只是把instance==nil换成了一个原子操作。有兴趣的同学可以查看其对应的源码。综上所述,以上就是Go语言实现单例模式的几种常见方式。经过比较,我们可以得出一个结论,最推荐的方式是使用once.Do来实现。sync.Once包帮助我们隐藏了一些细节,但是它可以使代码的可读性得到很大的提高。
