目录

GO语言实现设计模式-单例模式

单例模式的目的是为了保证一个类仅有一个实例,并提供一个访问它的全局访问点.

单例模式

单例模式的两种表现形式。

  • 饿汉式:类加载时,就进行实例化。

  • 懒汉式,第一次引用类时才进行实例化。

饿汉式

单例对象是在包加载时立即被创建,所以这个方式叫作饿汉式

singleton 包在被导入时会自动初始化 instance 实例,使用时通过调用 singleton.GetSingleton() 函数即可获得 singleton 这个结构体的单例对象。

1
2
3
4
5
6
7
8
9
package singleton

type singleton struct{}

var instance = &singleton{}

func GetSingleton() *singleton {
    return instance
}

懒汉式

懒汉式模式下实例只有在第一次被使用时才被创建。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package singleton

type singleton struct{}

var instance *singleton

func GetSingleton() *singleton {
    if instance == nil {
        instance = &singleton{}
    }
    return instance
}

并发安全

懒汉式单例模式并发安全问题,比如像下面这样。

使用了锁机制也带来了一些问题,这让每次调用 GetSingleton() 时程序都会进行加锁、解锁的步骤,从而导致程序性能的下降。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package singleton

import "sync"

type singleton struct{}

var instance *singleton

var mu sync.Mutex

func GetSingleton() *singleton {
    mu.Lock()
    defer mu.Unlock()
    if instance == nil {
        instance = &singleton{}
    }
    return instance
}

sync/atomic

带锁的单例模式是一个不错的方法,但是还并不是很完美。如果使用 sync/atomic 包的话,我们可以原子化加载并设置一个标志,该标志表明我们是否已初始化实例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import "sync"
import "sync/atomic"

var initialized uint32
...

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
}

不过这样有点繁琐,我们可以通过 Go 标准库 sync 包中提供的 Once 方法,让我们写出更加优雅的代码。

sync.Once

sync 库中的 Once 方法,它能保证某个操作仅且只执行一次。

Go标准库的部分源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Once is an object that will perform exactly one action.
type Once struct {
	// done indicates whether the action has been performed.
	// It is first in the struct because it is used in the hot path.
	// The hot path is inlined at every call site.
	// Placing done first allows more compact instructions on some architectures (amd64/x86),
	// and fewer instructions (to calculate offset) on other architectures.
	done uint32
	m    Mutex
}

func (o *Once) Do(f func()) {
	if atomic.LoadUint32(&o.done) == 0 { // check
		// Outlined slow-path to allow inlining of the fast-path.
		o.doSlow(f)
	}
}

func (o *Once) doSlow(f func()) {
	o.m.Lock()                          // lock
	defer o.m.Unlock()
	
	if o.done == 0 {                    // check
		defer atomic.StoreUint32(&o.done, 1)
		f()
	}
}

once.Do() 的用法如下:

1
2
3
once.Do(func() {
    // 在这里执行安全的初始化
})

代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package singleton

import "sync"

type singleton struct{}

var instance *singleton

var once sync.Once

func GetSingleton() *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}

Once 是一个结构体,在执行 Do 方法的内部通过 atomic 操作和加锁机制来保证并发安全,且 once.Do 能够保证多个 goroutine 同时执行时 &singleton{} 只被创建一次。

结论

以上几种单例模式的常用套路中,使用 sync.Once 包是安全地实现此目标的首选方式,sync.Once 包帮我们隐藏了部分细节,却可以让代码可读性得到很大提升。