单例模式采用了 饿汉式 和 懒汉式 两种实现,个人其实更倾向于饿汉式的实现,简单,并且可以将问题及早暴露,懒汉式虽然支持延迟加载,但是这只是把冷启动时间放到了第一次使用的时候,并没有本质上解决问题,并且为了实现懒汉式还不可避免的需要加锁。
单例模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式,是一种很常见的软件设计模式,在他的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。
单例模式确保某一个类只有一个实例。为什么要确保一个类只有一个实例?有什么时候才需要用到单例模式呢?听起来一个类只有一个实例好像没什么用呢!那我们来举个例子。比如我们的APP中有一个类用来保存运行时全局的一些状态信息,如果这个类实现不是单例的,那么App里面的组件能够随意的生成多个类用来保存自己的状态,等于大家各玩各的,那这个全局的状态信息就成了笑话了。而如果把这个类实现成单例的,那么不管App的哪个组件获取到的都是同一个对象(比如Application类,除了多进程的情况下)。
单例模式的实现主要有2种方式:
1.懒汉模式
2.饿汉模式
先说一下什么是懒汉模式吧,从懒汉这两个字,我们就能知道,这个人很懒,所以他不可能在未使用实例时就创建了对象,他肯定会在使用时才会创建实例,这个好处的就在于,只有在使用的时候才会创建该实例。下面我们一起来看看他的实现:
注意:以下我写的代码,除了GetInstance方法外其他都使用的小写字母开头,原因如下:
golang中根据首字母的大小写来确定可以访问的权限。无论是方法名、常量、变量名还是结构体的名称,如果首字母大写,则可以被其他的包访问;如果首字母小写,则只能在本包中使用。可以简单的理解成,首字母大写是公有的,首字母小写是私有的。
这里type singleton struct {}我们如果使用大写,那么我们写的这些方法就没有意义了,其他包就可以通过s := &singleton{}创建多个实例,单例模式就显得很没有意义了,所以这里一定要注意一下哦~~~
不加锁实现:这种方法是会存在线程安全问题的,在高并发的时候会有多个线程同时掉这个方法,那么都会检测instance为nil,这样就会导致创建多个对象,所以这种方法是不推荐的
package one
type singleton struct {
}
var instance *singleton
func GetInstance() *singleton {
if instance == nil{
instance = new(singleton)
}
return instance
}
整个方法加锁:这里对整个方法进行了加锁,这种可以解决并发安全的问题,但是效率就会降下来,每一个对象创建时都是进行加锁解锁,这样就拖慢了速度,所以不推荐这种写法。
type singleton struct {
}
var instance *singleton
var lock sync.Mutex
func GetInstance() *singleton {
lock.Lock()
defer lock.Unlock()
if instance == nil{
instance = new(singleton)
}
return instance
}
创建方法时进行锁定:这种方法也是线程不安全的,虽然我们加了锁,多个线程同样会导致创建多个实例,所以这种方式也不是推荐的。
type singleton struct {
}
var instance *singleton
var lock sync.Mutex
func GetInstance() *singleton {
if instance == nil{
lock.Lock()
instance = new(singleton)
lock.Unlock()
}
return instance
}
这里在上面的代码做了改进,只有当对象未初始化的时候,才会有加锁和减锁的操作。但是又出现了另一个问题:每一次访问都要检查两次
为了解决"双重检索中每一次访问都要检查两次"这个问题,我们可以使用golang标准包中的方法进行原子性操作;
这里使用了sync.Once的Do方法可以实现在程序运行过程中只运行一次其中的回调,这样就可以只创建了一个对象,这种方法是推荐的~~~。
type singleton struct {
}
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
once.Do(func() {
instance = new(singleton)
})
return instance
}
有懒汉模式,当然还要有饿汉模式啦,看了懒汉的模式,饿汉模式我们很好解释了,因为他饿呀,所以很着急的就创建了实例,不用等到使用时才创建,这样我们每次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象。比较适用于:如果某个单例使用的次数少,并且创建单例消息的资源比较多,那么就需要实现单例的按需创建,这个时候懒汉模式就是一个不错的选择。不过也有缺点,饿汉模式将在包加载的时候就会创建单例对象,当程序中用不到该对象时,浪费了一部分空间,但是相对于懒汉模式,不需要进行了加锁操作,会更安全,但是会减慢启动速度。
以下这两种方法都可以,第一种我们采用创建一个全局变量的方式来实现,第二种我们使用init包加载的时候创建实例,这里两个都可以,不过根据golang的执行顺序,全局变量的初始化函数会比包的init函数先执行,没有特别的差距。
type singleton struct {
}
var instance = new(singleton)
func GetInstance() *singleton{
return instance
}
type singleton struct {
}
var instance *singleton
func init() {
instance = new(singleton)
}
func GetInstance() *singleton{
return instance
}
代码实现:
package singleton
// Singleton 饿汉式单例
type Singleton struct{}
var singleton *Singleton
func init() {
singleton = &Singleton{}
}
// GetInstance 获取实例
func GetInstance() *Singleton {
return singleton
}
单元测试:
package singleton_test
import (
"testing"
singleton "github.com/mohuishou/go-design-pattern/01_singleton"
"github.com/stretchr/testify/assert"
)
func TestGetInstance(t *testing.T) {
assert.Equal(t, singleton.GetInstance(), singleton.GetInstance())
}
func BenchmarkGetInstanceParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if singleton.GetInstance() != singleton.GetInstance() {
b.Errorf("test fail")
}
}
})
}
代码实现:
package singleton
import "sync"
var (
lazySingleton *Singleton
once = &sync.Once{}
)
// GetLazyInstance 懒汉式
func GetLazyInstance() *Singleton {
if lazySingleton == nil {
once.Do(func() {
lazySingleton = &Singleton{}
})
}
return lazySingleton
}
可以看到直接 init 获取的性能要好一些
▶ C:\Users\laili\sdk\go1.15\bin\go.exe test -benchmem -bench="." -v
=== RUN TestGetLazyInstance
--- PASS: TestGetLazyInstance (0.00s)
=== RUN TestGetInstance
--- PASS: TestGetInstance (0.00s)
goos: windows
goarch: amd64
pkg: github.com/mohuishou/go-design-pattern/01_singleton
BenchmarkGetLazyInstanceParallel
BenchmarkGetLazyInstanceParallel-4 535702941 2.24 ns/op 0 B/op
0 allocs/op
BenchmarkGetInstanceParallel
BenchmarkGetInstanceParallel-4 1000000000 0.586 ns/op 0 B/op
0 allocs/op
PASS
ok github.com/mohuishou/go-design-pattern/01_singleton 3.161s