Golang singleton pattern and sync.Once

Golang singleton pattern and sync.Once

background

The singleton pattern can be said to be one of the simplest design patterns. The function is very simple: a type of thing is instantiated only once, there is only one instance globally, and a method is provided to obtain the instance.

In Golang, the effect that a variable or an instance is initialized only once initcan be achieved through a function. When a package is imported, the function will be executed once initand no matter how many times the same package is imported, it will only be executed once.

However, the singleton pattern that this article mainly wants to discuss is to initialize when it is needed for the first time, that is, lazy initialization.

Not so good singleton implementation

// bad_singleton.go


package main




import (
"sync"
)




var svcMu sync.Mutex
var svc *Svc




type Svc struct {
Num int
}




func GetSvc() *Svc {
if svc == nil { // 这一步判断不是并发安全的
svcMu.Lock()
defer svcMu.Unlock()
if svc == nil {
svc = &Svc{Num: 1}
svc = &Svc{}
svc.Num = 1



    }
}

return svc




}复制代码

}

svcMu.Lock()Note that the statement before executing the mutex is if svc == nil not concurrently safe, that is, in the scenario of multiple goroutines calling concurrently, one of the goroutines is svcin the process of initializing the variable, and other goroutines here judge that the result is svcnot equal to the same. It nildoes not mean that svcinitialization must be completed.

Because in the absence of explicit synchronization, the compiler and CPU are free to rearrange the order of instructions that access memory on the basis of ensuring serial consistency within each goroutine.

For example svc = &Svc{Num: 1}, this line seems to be just an execution statement, and an implementation after rearrangement may look like the following:

svc = &Svc{}
svc.Num = 1复制代码

It can be seen that it does nilnot mean that the initialization must be completed, so the above example is a bad singleton implementation.

Better singleton implementation

// good_singleton.go




package main




import (
"sync"
)




var svcOnce sync.Once
var svc *Svc




type Svc struct {
Num int
}




func GetSvc() *Svc {
svcOnce.Do(func() {
svc = &Svc{Num: 1}
})



return svc




}复制代码

}

sync.OnceThe provided Domethod only executes the incoming function once no matter how many times it is called, so why is it a better practice to use the Domethod to perform initialization instead of a layer ?if svc == nil sync.Once

// sync.Once 源码




package sync




import (
"sync/atomic"
)




type Once struct {
done uint32
m    Mutex
}




func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 { // 这步是判断是否已经完成初始化的关键
o.doSlow(f)
}
}




func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}复制代码

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

The official sync.Onceimplementation is very short and concise. Among them atomic.LoadUint32(&o.done) == 0is the key step. The atomic operation statement is used here, which ensures that it is safe even in concurrent scenarios, and the reading and writing of data are complete.

o.done的值为0时表示未进行初始化或正在初始化中,只有等于1时才表示初始化已经完成,即f()执行完成后由defer atomic.StoreUint32(&o.done, 1)语句给o.done赋值1;也就是o.done作为是否完成初始化的标识,可能的值只有前面说的两个,为0时则加锁并尝试初始化流程,反之则视为已完成初始化直接跳过,这样就完美兼顾了效率与并发安全。

由此可见sync.Once内置的初始化完成标识判断远比if svc == nil 靠谱,因此像上面这样使用sync.Once实现单例模式是最推荐的方式。

额外推荐

实则开发中用到的设计模式经常不止一种,越是复杂大型的项目就越需要使用更多合适的模式来优化代码。

下面要推荐的是RefactoringGuru。这是我所见过最好的设计模式教程,是国外创建的一个教程网站,有中文站点,图文并茂地介绍每一种模式的结构、关系和逻辑,
最重要的是示例代码方面囊括了常见的几种主流编程语言,是个适合多数程序员学习设计模式的好地方!

下图是设计模式的目录页面(是不是很图文并茂呢):

结语

以上为本人学习和实践的一些总结,如有错漏还请不吝赐教。

参考

《Go程序设计语言》9.5 延迟初始化:sync.Once 网络版
Go 单例模式讲解和代码示例

本文来源: Golang 单例模式与sync.Once

Guess you like

Origin juejin.im/post/7150321573314969613