Go Singleton 模式的實(shí)現(xiàn)
背景
在日常開發(fā)中,我們經(jīng)常會(huì)用到單例模式,比如啟動(dòng)時(shí)創(chuàng)建一個(gè)訪問數(shù)據(jù)庫(kù)的客戶端,或者運(yùn)行時(shí)創(chuàng)建一個(gè)訪問第三方服務(wù)的客戶端。啟動(dòng)時(shí)的單例問題比較簡(jiǎn)單,因?yàn)椴淮嬖诓l(fā)問題,直接在 main 函數(shù)創(chuàng)建即可。
package main
func main() {
Init()
}
var client *Client
func Init() {
cilent, err = New(...)
if err != nil {
panic(err)
}
}
如果創(chuàng)建失敗則直接 panic 重新啟動(dòng)服務(wù)。而運(yùn)行時(shí)的單例問題情況有所不同,我們一般會(huì)使用 sync.Once 來實(shí)現(xiàn)。
var client *Client
var once sync.Once
func GetClient() Client {
once.Do(func() {
cilent, err = New(...)
if err != nil {
//
}
})
return client
}
雖然 sync.Once 可以保證并發(fā)情況下只執(zhí)行一次,但是這個(gè)只執(zhí)行一次也會(huì)帶來一個(gè)問題,那就是如果執(zhí)行失敗了再也不會(huì)再執(zhí)行了。下面是 go1.24.3 中 sync.Once 的實(shí)現(xiàn)。
type Once struct {
done atomic.Uint32
m Mutex
}
func (o *Once) Do(f func()) {
if o.done.Load() == 0 {
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done.Load() == 0 {
defer o.done.Store(1)
f()
}
}
可以看到不管 f 是否執(zhí)行成功,Go 都會(huì)設(shè)置 done 為 1,所以如果 f 創(chuàng)建客戶端失敗,那么后面也不會(huì)調(diào)用了。但是存在這樣的場(chǎng)景,我們?cè)谔幚碚?qǐng)求時(shí)會(huì)創(chuàng)建一個(gè)單例去做一些事情,但因?yàn)檫@不是關(guān)鍵路徑,所以執(zhí)行失敗時(shí)不能 panic,而是返回錯(cuò)誤并希望下次還能重新走這個(gè)流程。所以我們需要實(shí)現(xiàn)一個(gè)單例模式,每次按需實(shí)時(shí)獲取,并且可以保證創(chuàng)建失敗時(shí)還可以重新執(zhí)行創(chuàng)建流程。
實(shí)現(xiàn) 1
type Singleton[T any] struct {
mu sync.Mutex
loaded bool
loader func() T
data T
}
func (in *Singleton[T]) Get() T {
in.mu.Lock()
if !in.loaded && in.loader != nil {
in.mu.Unlock()
in.Set(in.loader())
in.mu.Lock()
}
defer in.mu.Unlock()
return in.data
}
func (in *Singleton[T]) Set(data T) {
in.mu.Lock()
defer in.mu.Unlock()
in.loaded = true
in.data = data
}
這種方式實(shí)現(xiàn)的思路比較清晰簡(jiǎn)單,但是性能相對(duì)來說不太好,因?yàn)榈谝粋€(gè)創(chuàng)建成功后后續(xù)每次獲取時(shí)都需要加鎖,如果并發(fā)量大的會(huì)引起一定時(shí)間的代碼阻塞。所以嘗試優(yōu)化這部分的邏輯。
實(shí)現(xiàn) 2
type F[T any] func() (*T, error)
type singleton[T any] struct {
factory F[T]
instance *T
mutex sync.Mutex
}
func (s *singleton[T]) Get() (*T, error) {
if s.instance != nil {
return s.instance, nil
}
s.mutex.Lock()
defer s.mutex.Unlock()
if s.instance != nil {
return s.instance, nil
}
result, err := s.factory()
if err != nil {
returnnil, err
}
s.instance = result
return result, nil
}
在 Get 的一開始先判斷是否已經(jīng)創(chuàng)建過了,如果是則直接返回,避免了加鎖,這個(gè)看起來解決了問題,但是同時(shí)帶來了一個(gè)比較隱晦的問題,這種方式無(wú)法保證內(nèi)存可見性,也就是說當(dāng)讀者看到 s.instance 非空時(shí),不代表 s.instance 指向的實(shí)例是完成的,即初始化完成的。看一個(gè)例子。
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
type MyStruct struct {
Field int
}
for {
var flag atomic.Bool
var ptr *MyStruct
var wg sync.WaitGroup
wg.Add(2)
// Goroutine A
gofunc() {
defer wg.Done()
data := &MyStruct{Field: 42}
ptr = data // 原子操作,但不保證 data 內(nèi)容立即對(duì)其他線程可見
}()
// Goroutine B
gofunc() {
defer wg.Done()
if ptr != nil {
field := ptr.Field
if field == 0 {
fmt.Println(field)
flag.Store(true)
}
}
}()
wg.Wait()
if flag.Load() {
println("flag is true")
break
}
}
}
執(zhí)行上面的代碼,最終會(huì)輸出 flag is true,說明 ptr 非空時(shí),ptr.Field 卻是 0?;氐絾卫龑?shí)現(xiàn)的代碼中測(cè)試也是存在類似的問題。
package singleton
import (
"sync"
"testing"
)
type Dummy struct {
Ptr *string
}
func factory() (*Dummy, error) {
ptr := "test"
return &Dummy{
Ptr: &ptr,
}, nil
}
func TestConcurrent(t *testing.T) {
for {
singleton := New(factory)
var flag bool
var ptr *Dummy
var wg sync.WaitGroup
len := 10
wg.Add(len)
for i := 0; i < len; i++ {
gofunc() {
defer wg.Done()
ptr, _ = singleton.Get()
if ptr.Ptr == nil {
flag = true
}
}()
}
wg.Wait()
if flag {
t.Fatal("singleton should not be nil")
}
}
}
上面的代碼最終會(huì)輸出 singleton should not be nil。說明當(dāng) singleton.Get 觀察到 s.instance 非空時(shí) s.instance 指向到單例對(duì)象并沒有完成構(gòu)造。
實(shí)現(xiàn) 3
為了實(shí)現(xiàn)內(nèi)存的可見性,我們需要使用 Go 提供的 API。
package singleton
import (
"sync"
"sync/atomic"
)
type F[T any] func() (*T, error)
type singleton[T any] struct {
factory F[T]
instance atomic.Pointer[T]
mutex sync.Mutex
}
func (s *singleton[T]) Get() (*T, error) {
if s.instance.Load() != nil {
return s.instance.Load(), nil
}
s.mutex.Lock()
defer s.mutex.Unlock()
if s.instance.Load() != nil {
return s.instance.Load(), nil
}
result, err := s.factory()
if err != nil {
returnnil, err
}
s.instance.Store(result)
return result, nil
}
上面代碼中,Load 會(huì)保證 Store 之前的寫入全部可見,也就是說當(dāng) Load 返回非空指針時(shí),Store 寫入的指針以及 s.factory 構(gòu)造的結(jié)構(gòu)體已經(jīng)全部同步完成。具體可以參考這里 https://github.com/theanarkh/singleton。