Golang 常見設(shè)計模式之單例模式
之前我們已經(jīng)看過了 Golang 常見設(shè)計模式中的裝飾和選項模式,今天要看的是 Golang 設(shè)計模式里最簡單的單例模式。單例模式的作用是確保無論對象被實例化多少次,全局都只有一個實例存在。根據(jù)這一特性,我們可以將其應(yīng)用到全局唯一性配置、數(shù)據(jù)庫連接對象、文件訪問對象等。Go 語言實現(xiàn)單例模式的方法有很多種,下面我們就一起來看一下。
餓漢式
餓漢式實現(xiàn)單例模式非常簡單,直接看代碼:
package singleton
type singleton struct{}
var instance = &singleton{}
func GetSingleton() *singleton {
return instance
}
singleton 包在被導(dǎo)入時會自動初始化 instance 實例,使用時通過調(diào)用 singleton.GetSingleton () 函數(shù)即可獲得 singleton 這個結(jié)構(gòu)體的單例對象。
這種方式的單例對象是在包加載時立即被創(chuàng)建,所以這個方式叫作餓漢式。與之對應(yīng)的另一種實現(xiàn)方式叫作懶漢式,懶漢式模式下實例會在第一次被使用時被創(chuàng)建。
需要注意的是,盡管餓漢式實現(xiàn)單例模式的方式簡單,但大多數(shù)情況下并不推薦。因為如果單例實例化時初始化內(nèi)容過多,會造成程序加載用時較長。
懶漢式
接下來我們再來看下如何通過懶漢式實現(xiàn)單例模式:
package singleton
type singleton struct{}
var instance *singleton
func GetSingleton() *singleton {
if instance == nil {
instance = &singleton{}
}
return instance
}
相較于餓漢式的實現(xiàn),懶漢式將實例化 singleton 結(jié)構(gòu)體部分的代碼移到了 GetSingleton () 函數(shù)內(nèi)部。這樣能夠?qū)ο髮嵗牟襟E延遲到 GetSingleton () 第一次被調(diào)用時。
不過通過 instance == nil 的判斷來實現(xiàn)單例并不十分可靠,如果有多個 goroutine 同時調(diào)用 GetSingleton () 就無法保證并發(fā)安全。
支持并發(fā)的單例
如果你使用 Go 語言寫過并發(fā)編程,應(yīng)該很快能想到該如何解決懶漢式單例模式并發(fā)安全問題,比如像下面這樣:
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
}
上面代碼的修改是通過加鎖機(jī)制,即在 GetSingleton () 函數(shù)最開始加了如下兩行代碼:
mu.Lock()
defer mu.Unlock()
加鎖的機(jī)制可以有效保證這個實現(xiàn)單例模式的函數(shù)是并發(fā)安全的。
不過使用了鎖機(jī)制也帶來了一些問題,這讓每次調(diào)用 GetSingleton () 時程序都會進(jìn)行加鎖、解鎖的步驟,從而導(dǎo)致程序性能的下降。
雙重鎖定
加鎖會導(dǎo)致程序性能下降,但又不用鎖又無法保證程序的并發(fā)安全。為了解決這個問題有人提出了雙重鎖定(Double-Check Locking)的方案:
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 判斷,通過這種方式來兼顧性能和安全兩個方面。不過這讓代碼看起來有些奇怪,外層已經(jīng)判斷了 instance == nil,但是加鎖后又進(jìn)行了第二次 instance == nil 判斷。
其實外層的 instance == nil 判斷是為了提高程序的執(zhí)行效率,免去原來每次調(diào)用 GetSingleton () 都上鎖的操作,將加鎖的粒度更加精細(xì)化。簡單說就是如果 instance 已經(jīng)存在,則無需進(jìn)入 if 邏輯,程序直接返回 instance 即可。而內(nèi)層的 instance == nil 判斷則考慮了并發(fā)安全,考慮到萬一在極端情況下,多個 goroutine 同時走到了加鎖這一步,內(nèi)層判斷會在這里起到作用。
Gopher 慣用方案
雖然雙重鎖定機(jī)制兼顧和性能和并發(fā)安全,但顯然代碼有些丑陋,不符合廣大 Gopher 的期待。好在 Go 語言在 sync 包中提供了 Once 機(jī)制能夠幫助我們寫出更加優(yōu)雅的代碼:
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 是一個結(jié)構(gòu)體,在執(zhí)行 Do 方法的內(nèi)部通過 atomic 操作和加鎖機(jī)制來保證并發(fā)安全,且 once.Do 能夠保證多個 goroutine 同時執(zhí)行時 &singleton {} 只被創(chuàng)建一次。
其實 Once 并不神秘,其內(nèi)部實現(xiàn)跟上面使用的雙重鎖定機(jī)制非常類似,只不過把 instance == nil 換成了 atomic 操作,感興趣的同學(xué)可以查看下其對應(yīng)源碼。
總結(jié)
以上就是 Go 語言中實現(xiàn)單例模式的幾種常用套路,經(jīng)過對比可以得出結(jié)論,最推薦的方式是使用 once.Do 來實現(xiàn),sync.Once 包幫我們隱藏了部分細(xì)節(jié),卻可以讓代碼可讀性得到很大提升。