前言
单例模式是最简单的一个模式,指的是全局只有一个实例,并且它负责创建自己的对象。
单例模式不仅有利于减少内存开支,还有减少系统性能开销、防止多个实例产生冲突等优点。 因为单例模式保证了实例的全局唯一性,而且只被初始化一次,所以比较适合全局共享一个实例,且只需要被初始化一次的场景,例如数据库实例、全局配置、全局任务池等。
两个方式
单例模式又分为饿汉方式和懒汉方式。
饿汉方式
饿汉方式指全局的单例实例在包被加载时创建,
go中有两种常见写法,一种是利用全局变量的初始化:
package singleton
type singleton struct {
}
var instance = &singleton{}
func getInstance() *singleton {
return instance
}
或者可以通过使用init函数,init函数会在package被加载时执行一次,从而避开了并发:
package singleton
type singleton struct {
}
var instance *singleton
func getInstance() *singleton {
return instance
}
func init() {
instance = &singleton{}
}
我们可以写个测试验证确实getInstance返回的是同一个实例:
package singleton
import (
"github.com/stretchr/testify/assert"
"testing"
)
func Test_getInstance(t *testing.T) {
assert.Equal(t, getInstance(), getInstance())
assert.Equal(t, getInstance(), instance)
}
$ go test -v
=== RUN Test_getInstance
--- PASS: Test_getInstance (0.00s)
PASS
ok ……/demo 1.660s
饿汉模式没有线程安全性问题,不需要考虑懒加载,获取实例的效率特高。缺点是如果初始化比较耗时,会导致程序加载时间比较长;即使没有用到这个对象,只要import路径里有它也照样要进行初始化。
PS:业务中看到有的人会在初始化代码中写一些外部依赖的代码,比如ping个网络地址,甚至还写个如果失败的话就panic,这就给本地测试等带来了很大的麻烦。这种饿汉模式简直是灾难。
懒汉模式
现在流行懒汉模式“延迟加载”,换句话说,在第一次使用这个单例类的时候才实际创建并初始化这个实例。
一个最简单的懒汉模式示例代码如下:
package singleton
type singleton struct {
}
var instance *singleton
func getInstance() *singleton {
if instance == nil {
instance = &singleton{}
}
return instance
}
package singleton
import (
"github.com/stretchr/testify/assert"
"testing"
)
func Test_getInstance(t *testing.T) {
assert.Nil(t, instance)
assert.Equal(t, getInstance(), getInstance())
assert.NotNil(t, instance)
assert.Equal(t, getInstance(), instance)
}
这个代码在单线程的环境下工作良好,但是在多线程环境下会有并发问题,如果两个线程同时getInstance ,可能会创建出两个实例。
经典的线程安全的懒汉模式go语言实现示例如下:
package singleton
import "sync"
type singleton struct {
}
var (
instance *singleton
lock sync.Mutex
)
func getInstance() *singleton {
if instance == nil {
lock.Lock()
if instance == nil {
instance = &singleton{}
}
lock.Unlock()
}
return instance
}
当发现instance为nil后,会加锁准备创建对象,加锁成功后必须重新检查是否为nil,应为可能被并发的另一个线程抢先创建了。
在go语言中,我们还可以使用Once实现只初始化一次(看着好像更高端了):
package singleton
import "sync"
type singleton struct {
}
var (
instance *singleton
once sync.Once
)
func getInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
Once可以保证只执行一次,且在执行时阻塞同时想要执行的其他线程。
肯定有小可爱觉得这样的写法更好:
func getInstance() *singleton {
if instance == nil {
once.Do(func() {
instance = &singleton{}
})
}
return instance
}
但是实际上,通过基准测试发现,两种写法的性能没啥差。
结语
好像没什么好写的,这是最简单的设计模式,就算没学过设计模式也几乎在实际工作中会使用到。了解一下即可。
|