之前我們已經看過了 Golang 常見設計模式中的裝飾和選項模式,今天要看的是 Golang 設計模式裡最簡單的單例模式。單例模式的作用是確保無論物件被範例化多少次,全域性都只有一個範例存在。根據這一特性,我們可以將其應用到全域性唯一性設定、資料庫連線物件、檔案存取物件等。Go 語言實現單例模式的方法有很多種,下面我們就一起來看一下。
餓漢式實現單例模式非常簡單,直接看程式碼:
package singleton
type singleton struct{}
var instance = &singleton{}
func GetSingleton() *singleton {
return instance
}
singleton 包在被匯入時會自動初始化 instance 範例,使用時通過呼叫 singleton.GetSingleton() 函數即可獲得 singleton 這個結構體的單例物件。
這種方式的單例物件是在包載入時立即被建立,所以這個方式叫作餓漢式。與之對應的另一種實現方式叫作懶漢式,懶漢式模式下範例會在第一次被使用時被建立。
需要注意的是,儘管餓漢式實現單例模式的方式簡單,但大多數情況下並不推薦。因為如果單例範例化時初始化內容過多,會造成程式載入用時較長。
接下來我們再來看下如何通過懶漢式實現單例模式:
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 語言寫過並行程式設計,應該很快能想到該如何解決懶漢式單例模式並行安全問題,比如像下面這樣:
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() 函數最開始加了如下兩行程式碼:
mu.Lock()
defer mu.Unlock()
加鎖的機制可以有效保證這個實現單例模式的函數是並行安全的。
不過使用了鎖機制也帶來了一些問題,這讓每次呼叫 GetSingleton() 時程式都會進行加鎖、解鎖的步驟,從而導致程式效能的下降。
加鎖會導致程式效能下降,但又不用鎖又無法保證程式的並行安全。為了解決這個問題有人提出了雙重鎖定(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 判斷,通過這種方式來兼顧效能和安全兩個方面。不過這讓程式碼看起來有些奇怪,外層已經判斷了 instance == nil,但是加鎖後又進行了第二次 instance == nil 判斷。
其實外層的 instance == nil 判斷是為了提高程式的執行效率,免去原來每次呼叫 GetSingleton() 都上鎖的操作,將加鎖的粒度更加精細化。簡單說就是如果 instance 已經存在,則無需進入 if 邏輯,程式直接返回 instance 即可。而內層的 instance == nil 判斷則考慮了並行安全,考慮到萬一在極端情況下,多個 goroutine 同時走到了加鎖這一步,內層判斷會在這裡起到作用。
雖然雙重鎖定機制兼顧和效能和並行安全,但顯然程式碼有些醜陋,不符合廣大 Gopher 的期待。好在 Go 語言在 sync 包中提供了 Once 機制能夠幫助我們寫出更加優雅的程式碼:
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 包幫我們隱藏了部分細節,卻可以讓程式碼可讀性得到很大提升。