适用场景

一些实例化很耗费资源的类, 比如线程池, 协程池
一些只允许存在一个实例的类, 比如日志管理模块

实现方式

饿汉式

一开始就完成单例的初始化

实现流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var s *demo

func init() {
s = newDemo()
}

type demo struct {
}
func newDemo() *demo {
return &demo{}
}
func (s *demo)Work {

}

func GetDemo() *demo {
return s
}

代码规范性讨论

  • demo是包内不可导出的类型, 在包内获取到了, 也不能作为入参或者出参进行传递
  • demo对外暴露, 混淆了不可导出类的边界与定位
    处理方法
    在不可导出单例类基础上 包上一层interface, 然后将这个inerface作为返回参数类型
1
2
3
4
5
6
type Instace interface {
Work()
}
func GetInstance() Instance {
return s
}

懒汉式

要用的时候再初始化

实现流程

1.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var s *singleton

type singleton struct {
}

func newSingleton() *singleton {
return &singleton{}
}

func (s *singleton) Work() {
}

type Instance interface {
Work()
}

func GetInstance() Instance {
if s == nil {
s = newSingleton()
}
return s
}

但是这样存在并发问题, 可能被初始化两次, 与设计违背

2.0

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
28
var (
s *singleton
mux sync.Mutex
)

type singleton struct {
}

func newSingleton() *singleton {
return &singleton{}
}

func (s *singleton) Work() {
}

type Instance interface {
Work()
}

func GetInstance() Instance {
mux.Lock()
defer mux.Unlock()
if s == nil {
s = newSingleton()
}
return s
}

调用Get方法的时候先拿锁, 这保证了并发安全,但是这个单例只需要初始化一次, 而每次Get都要加锁, 存在损耗

3.0

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
28
29
30
var (
s *singleton
mux sync.Mutex
)

type singleton struct {
}

func newSingleton() *singleton {
return &singleton{}
}

func (s *singleton) Work() {
}

type Instance interface {
Work()
}

func GetInstance() Instance {
if s != nil {
return s
}
mux.Lock()
defer mux.Unlock()

s = newSingleton()

return s
}

为了避免每次Get都要拿锁, 在加锁前进行一次判断, 如果已经初始化了就直接返回

但是, 这样也存在漏洞, 比如还没有初始化的时候, 两个goroutine同时到达了mux.Lock()这里去拿锁, 只有一个g拿到了锁, 执行完初始化后, 另一个g抢到锁也会进行一次初始化

4.0

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
28
29
30
31
32
33

var (
s *singleton
mux sync.Mutex
)

type singleton struct {
}

func newSingleton() *singleton {
return &singleton{}
}

func (s *singleton) Work() {
}

type Instance interface {
Work()
}

func GetInstance() Instance {
if s != nil {
return s
}
mux.Lock()
defer mux.Unlock()
if s != nil {
return s
}
s = newSingleton()

return s
}

进行一次 double check, 在加锁前进行一次检查, 看初始化状态, 如果初始化后就直接返回, 保证最大程度无锁

加锁后, 再进行一次check, 确保此时的单例仍然没有初始化过

5.0

实际上go的标准库已经有了sync.Once, 里面已经做了这些工作来保证单例

1
2
3
4
5
6
func GetInstance() Instance {
once.Do(func() {
s = newSingleton()
})
return s
}

对比

饿汉式在程序运行之处就完成初始化, 有可能这个单例对象一直用不到, 造成无谓的损耗

懒汉式在首次使用才进行初始化, 但是有可能我们的实例初始化的时候存在panic, fatal, 而这要在运行的时候才会直接暴露, 有可能造成更大的伤害

Sync.Once的实现

1
2
3
4
5
6
type Once struct {
// 表示是否已经执行
done uint32
// 锁, 并发场景下保护done智能串行访问
m Mutex
}

Once.Do

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func (o *Once) Do(f func()) {
// 锁外的第一次 check,读取 Once.done 的值
if atomic.LoadUint32(&o.done) == 0 {
o.doSlow(f)
}
}

func (o *Once) doSlow(f func()) {
// 加锁
o.m.Lock()
defer o.m.Unlock()
// double check
if o.done == 0 {
// 任务执行完成后,将 Once.done 标识为 1
defer atomic.StoreUint32(&o.done, 1)
// 保证全局唯一一次执行用户注入的任务
f()
}
}