定义
确保某一个类只有一个实例 ,而且自行实例化并向整个系统提供这个实例
使用场景
在实际开发中,为了节约系统资源,有时候需要确保系统中某个结构体只有唯一的一个实例,当这个实例创建成功后,无法再创建同一个类型的其他实例,所有的操作都只能基于这个唯一实例,为了确保对象的唯一性,可以通过单例模式来实现。
概述
这里我用一个例子来对单例模式进行解释说明。众所周知,女朋友只能有一个,那么我们如何利用代码来实现这个逻辑呢。
这里先创建一个GirFriend的接口,来避免返回一个包私有的指针
type GirlFriend interface {
Dating() //约会
}
定义一个包私有的结构体,girlFriend,并且实现Dating方法
type girlFriend struct {
name string
}
func (g *girlFriend) Dating() {
fmt.Printf("和%s去约会...\n", g.name)
}
最后我们需要定义一个全局的grilFriend的指针,并且定义一个创建单例的函数
var (
gf *girlFriend //或者首字母大写,NewGirlFriend直接初始化,不需要返回值了
)
func NewGirlFriend(name string) GirlFriend {
if gf == nil { // 如果不加这个判断,在NewGirlFriend多次调用时,gf会一直变化
gf = &girlFriend{
name: name,
}
} else {
fmt.Printf("还想脚踩两条船,no door ")
}
return gf
}
上面是单例模式,也就是只有一个实例指针(如初始化数据库连接)。
如果想初始化多个不同实例(如:根据name传值初始化不同实例)如下:
func NewGirlFriend(name string) GirlFriend {
GF_girlFriend = &girlFriend{
name: name,
}
return GF_girlFriend
}
可以写一个测试方法测试一下效果
func TestNewGirlFriend(t *testing.T) {
gf := NewGirlFriend("小铃铛")
gf.Dating()
gf1 := NewGirlFriend("大铃铛")
gf1.Dating()
}
// 输出结果
=== RUN TestNewGirlFriend
和小铃铛去约会...
还想脚踩两条船,no door 和小铃铛去约会...
--- PASS: TestNewGirlFriend (0.00s)
PASS
我们可以看到,两次的调用,最后我们都是和最开始的女朋友去进行约会,是没办法脚踏两只船的,very Good !!!
单例模式的三个要点
- 某个结构体只能有一个实例
- 必须自行创建这个实例
- 必须向整个系统提供这个实例
饿汉式单例和懒汉式单例
上面的代码其实在并发的情况下是没办法保证创建出来的一定是一个单例,就比如
Goroutine1 这里称为G1,Goroutine2这里称为G2,同时去创建一个对象,加入G1在判断全局没有这个实例之后阻塞了3s,这个时候G2也去判断了,发现全局确实没有这个实例,那么G2就会去创建一个实例,3s之后G1阻塞结束,这个时候G1已经通过了判断,那么他也就会去创建这个实例,这个时候G1和G2拿到的实例就是完全不一样的了。(当然女朋友是不能共享的,这里没想到其他的例子,暂时用一下)
为了解决上述问题,我们有两种解决方法:饿汉式单例模式和懒汉式单例模式
饿汉式
饿汉式的实现方式就是在程序启动的时候,就把这个实例创建好,我们可以使用init函数,来保证程序启动的时候就会执行创建实例的方法。init函数会在程序运行先优先加载执行,这样我们就能保证他提前就加载好了。
func init(){
fmt.Printf("饿汉式加载.....")
NewGirlFriend("小铃铛")
}
测试方法
func TestNewGirlFriend(t *testing.T) {
gf := NewGirlFriend("中铃铛")
gf.Dating()
gf1 := NewGirlFriend("大铃铛")
gf1.Dating()
}
// 输出
饿汉式加载.....=== RUN TestNewGirlFriend
还想脚踩两条船,no door 和小铃铛去约会...
还想脚踩两条船,no door 和小铃铛去约会...
PASS
我们可以看到,饿汉时在Test方法执行之前,就已经进行了加载,纵使后面想和中铃铛,大铃铛约会,那也是不允许的。这样就确保了对象的唯一性
懒汉式
懒汉式的加载有点像我们最开始写的,在需要使用的时候进行加载,但是为了保证不会出现并发的情况,需要对创建的方法进行加锁。我们可以使用sync.Once来实现,sync.Once可以保证函数只会被执行一次
var (
gf *girlFriend
once sync.Once
)
func NewGirlFriend(name string) GirlFriend {
once.Do(func() {
gf = &girlFriend{
name: name,
}
})
return gf
}
定义了一个全局的sync.Once,并且将创建对象的方法,放在Do里面,这样就能保证Do里面的函数只会执行一次
测试方法
const count = 100
func TestParallelSingleton(t *testing.T) {
start := make(chan struct{})
wg := sync.WaitGroup{}
wg.Add(count)
gfs := [count]GirlFriend{}
for i := 0; i < 100; i++ {
go func(index int) {
<-start
gfs[index] = NewGirlFriend(fmt.Sprintf("小铃铛%d号", index))
gfs[index].Dating()
defer wg.Done()
}(i)
}
close(start)
wg.Wait()
}
// 输出
=== RUN TestParallelSingleton
和小铃铛1号去约会...
和小铃铛1号去约会...
和小铃铛1号去约会...
和小铃铛1号去约会...
和小铃铛1号去约会...
和小铃铛1号去约会...
这里模拟了多个goroutine同时去执行创建方法,我们可以看到最终输出的实例是同一个
总结
优点
提供了唯一实例的受控访问,所以可以严格控制何时访问
节约了系统资源,因为只有一个实例存在内存中
缺点
没有抽象层,所以单例模式拓展起来比较困难
职责过重,因为单例既提供了业务方法,又提供了创建对象的方法
现在很多语言的垃圾回收方式,会将创建好的实例但是又不被利用的实例当成垃圾,自动销毁。下次使用的时候需要重新创建,会导致单例实例的状态丢失