单例模式是用来控制类型实例的数量的,当需要确保一个类型只有一个实例时,就需要使用单例模式
由于要控制数量,那么可想而之只能把实例的访问进行收口,不能谁来了都能 new 一个出来,所以单例模式还会提供一个访问该实例的全局端口,一般都会命名个 GetInstance 之类的函数用作实例访问的端口。
又因为在什么时间创建出实例,单例模式又可以分裂出 饿汉模式
和 懒汉模式
,前者适用于在程序早期初始化时创建已经确定需要加载的类型实例,比如项目的数据库实例。后者其实就是延迟加载的模式,适合程序执行过程中条件成立才创建加载的类型实例。
饿汉模式
这个模式用 Go 语言实现时,借助 Go 的 init 函数来实现特别方便
下面用单例模式返回数据库连接实例,相信你们在项目里都见过类似代码。
package dao
// 饿汉式单例
// 注意定义非导出类型
type databaseConn struct {
...
}
var dbConn *databaseConn
func init() {
dbConn = &databaseConn{}
}
// GetInstance 获取实例
func Db() *databaseConn {
return dbConn
}
这里初始化数据库的细节咱们就不多费文笔了,实际情况肯定是从配置中心加载下来数据库连接配置再实例化数据库的连接对象。这里有人可能会有个问题,你这一个程序进程就只有一个数据连接实例,那这么多请求都用一个数据库连接行吗?
诶,这个是对数据库连接的抽象呀,这个实例会维护一个连接池,那里才是真正去请求数据库用的连接。是不是有点晕,有点晕去看看你们项目里这块的代码。一般会看到初始化实例时,让你设置最大连接数、闲置连接数和存活时间这样的连接池配置。
懒汉模式
懒汉模式,通俗点说就是延迟加载,不过这块特别注意,要考虑并发环境下,你的判断实例是否已经创建时,是不是用的当前读。在一些教设计模式的教程里,一般这种情况下会举一个例子用 Java 双重锁实现线程安全的单例模式,双重锁指的是 volatile 和 synchronized。
那么 Go 里边没有 volatile 这种机制,我们该怎么办呢?聪明的你一定能想得出,我们定义一个实例的状态变量,然后用原子操作 atomic.Load、atomic.Store 去读写这个状态变量,不就是实现了吗?像下面这样:
import (
"fmt"
"sync"
"sync/atomic"
)
var initialized uint32
type singleton struct {
}
var mu sync.Mutex
var instance *singleton
func GetInstance() *singleton {
if atomic.LoadUint32(&initialized) == 1 { // 原子操作
return instance
}
mu.Lock()
defer mu.Unlock()
if initialized == 0 {
instance = &singleton{}
atomic.StoreUint32(&initialized, 1)
}
return instance
}
确实,相当于把上面 Java 的例子翻译成用 Go 实现了,不过还有另外一种更 Go native 的写法,比这种写法更简练。如果用 Go 更惯用的写法,我们可以借助其sync库中自带的并发同步原语Once来实现:
import (
"fmt"
"sync"
)
type singleton struct {
}
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}