天天看點

Go 常見設計模式之單例模式

作者:馬士兵教育CTO

單例模式是設計模式中最簡單的一種模式,單例模式能夠確定無論對象被執行個體化多少次,全局都隻有一個執行個體存在。根據單例模式的特性,我們可以将其應用到全局唯一性配置、資料庫連接配接對象、檔案通路對象等。Go 語言有多種方式可以實作單例模式,我們今天就來一起學習下。

餓漢式

餓漢式實作單例模式非常簡單,直接看代碼:

go複制代碼package singleton

type singleton struct{}

var instance = &singleton{}

func GetSingleton() *singleton {
	return instance
}
           

singleton 包在被導入時會自動初始化 instance 執行個體,使用時通過調用 singleton.GetSingleton() 函數即可獲得 singleton 這個結構體的單例對象。

由于單例對象是在包加載時立即被建立出來,是以也就有了這個形象的名稱叫作餓漢式。與之對應的另一種實作方式叫作懶漢式,當執行個體被第一次使用時才會被建立。

需要注意的是,盡管餓漢式實作單例模式如此簡單,但大多數情況下仍不被推薦使用,因為如果單例執行個體化時初始化内容過多,可能造成程式加載用時較長。

懶漢式

接下來我們再來看下如何通過懶漢式實作單例模式:

go複制代碼package singleton

type singleton struct{}

var instance *singleton

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

相較于餓漢式的實作,我們把執行個體化 singleton 結構體部分的代碼移到了 GetSingleton() 函數内部。這樣一來,就将對象執行個體化的步驟延遲到了 GetSingleton() 被第一次調用時。

通過 instance == nil 的判斷來實作單例并不十分可靠,當有多個 goroutine 同時調用 GetSingleton() 時無法保證并發安全。

支援并發的單例

如果你用 Go 語言寫過并發程式設計,那麼應該可以很快想到解決懶漢式單例模式并發安全問題的方案:

go複制代碼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
}
           

我們對代碼的主要修改就是在 GetSingleton() 函數最開始加了如下兩行代碼:

go複制代碼mu.Lock()
defer mu.Unlock()
           

通過加鎖的機制,就可以保證這個實作單例模式的函數是并發安全的。

不過這樣也有些問題,因為用了鎖機制,每次調用 GetSingleton() 時程式都會進行加鎖、解鎖的步驟,這樣會導緻程式性能的下降。

雙重鎖定

加鎖導緻程式性能下降,但我們又不得不用鎖來保證程式的并發安全,于是有人想出了雙重鎖定(Double-Check Locking)的方案:

go複制代碼package singleton

import "sync"

type singleton struct{}

var instance *singleton

var mu sync.Mutex

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

		if instance == nil {
			instance = &singleton{}
		}
	}
	return instance
}
           

可以看到,所謂的雙重鎖定實際上就是在程式加鎖前又加了一層 instance == nil 判斷,這樣就兼顧了性能和安全兩個方面。

不過這段代碼看起來有些奇怪,既然外層已經判斷了 instance == nil,加鎖後卻又進行了第二次 instance == nil 判斷。其實外層的 instance == nil 判斷是為了提高程式的執行效率,因為如果 instance 已經存在,則無需進入 if 邏輯,程式直接傳回 instance 即可。這樣就免去了原來每次調用 GetSingleton() 都上鎖的操作,将加鎖的粒度更加精細化。而内層的 instance == nil 判斷則是考慮了并發安全,在極端情況下,多個 goroutine 同時走到了加鎖這一步,内層判斷就起到作用了。

Gopher 慣用方案

雖然我們通過雙重鎖定機制兼顧和性能和并發安全,但代碼有些醜陋,不符合廣大 Gopher 的期待。好在 Go 語言在 sync 包中提供了 Once 機制能夠幫助我們寫出更加優雅的代碼:

go複制代碼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{} 隻被建立一次。

其實 Once 并不神秘,其内部實作跟上面使用的雙重鎖定機制非常類似,隻不過把 instance == nil 換成了 atomic 操作,感興趣的同學可以檢視下其對應源碼。

總結

以上就是 Go 語言中實作單例模式的幾種常用套路,經過對比可以得出結論,最推薦的方式是使用 once.Do 來實作,sync.Once 包幫我們隐藏了部分細節,卻可以讓代碼可讀性得到很大提升。

繼續閱讀