Go 語言實現創建型設計模式 - 單例模式

01 介紹

單例模式(Singleton Pattern)是一種創建型設計模式,它確保一個類只有一個實例,並提供一個全局訪問點。

因爲它同時解決了兩個問題,所以它違反了單一職責原則。

02 使用場景

什麼場景適合使用單例模式呢?

某個類對於所有客戶端只有一個可用的實例

比如記錄應用程序的運行日誌,因爲記錄日誌的文件只有一個,所以只能有一個日誌類的實例向日志文件中寫入,否則會出現日誌內容互相覆蓋的問題。

需要更加嚴格地控制全局變量

所謂更加嚴格地控制全局變量,即使用單例模式確保一個類只有一個實例,除了該類自己以外,無法通過任何方式替換緩存的實例(控制全局變量)。

03 實現方式

在 Go 語言中,沒有類 Class 的概念,我們可以使用結構體 struct 替代。

  1. 定義一個私有變量,用於保存單例類的實例。

  2. 定義一個公有函數,用於獲取單例類的實例。

  3. 在公有函數中實現 “延遲實例化”。

04 Go 實現

實現單例模式,一般分爲三種方式,分別是急切實例化(餓漢式)、延遲實例化(懶漢式)和雙重檢查加鎖實例化。

此外,Go 標準庫 sync/once,也可用於實現單例模式。

急切實例化:

急切實例化(餓漢式)是指在導入包時自動創建實例,並且創建的實例會一直存儲在內存中,它是併發安全的,可以使用 init() 初始化函數實現。

一般用於實例佔用資源少,並且使用頻率高的場景。

type singletonV1 struct {
 
}

var instance *singletonV1

func init() {
 instance = new(singletonV1)
 fmt.Printf("%p\n", instance)
}

func GetInstance() *singletonV1 {
 return instance
}

延遲實例化:

延遲實例化(懶漢式)是指在導入包時不自動創建實例,而是在初次使用時,纔會創建實例。它不是併發安全的,可以通過加鎖確保協程的併發安全,但是會影響程序的性能。

一般用於實例佔用資源多,並且使用率低的場景。

非併發安全:

type singletonV2 struct {

}

var instance *singletonV2

func GetInstance() *singletonV2 {
 if instance == nil {
  instance = new(singletonV2)
 }
 fmt.Printf("%p\n", instance)
 return instance
}

併發安全:

type singletonV3 struct {

}

var instance *singletonV3

var mu sync.Mutex

func GetInstance() *singletonV3 {
 if instance == nil {
  mu.Lock()
  defer mu.Unlock()
  instance = new(singletonV3)
 }
 fmt.Printf("%p\n", instance)
 return instance
}

雙重檢查加鎖實例化:

雙重檢查加鎖實例化實際上是對通過鎖支持併發的延遲實例化的優化,減少鎖操作,降低性能損耗。

type singletonV4 struct {

}

var instance *singletonV4
var mu sync.Mutex

func GetInstance() *singletonV4 {
 if instance == nil {
  mu.Lock()
  defer mu.Unlock()
  if instance == nil {
   instance = new(singletonV4)
  }
 }
 fmt.Printf("%p\n", instance)
 return instance
}

閱讀上面這段代碼,第一次 nil 判斷,是爲了減少鎖操作,第二次 nil 判斷,是爲了確保只有一個爭搶到鎖的協程創建實例。

雙重檢查加鎖實例化(原子操作):

雙重檢查加鎖實例化的兩次檢查,我們還可以將第一次 nil 判斷,改爲通過使用 sync/atomic 包的原子操作判斷,決定是否需要進行爭搶鎖。

type singletonV5 struct {

}

var instance *singletonV5

var mu sync.Mutex

var done uint32

func GetInstance() *singletonV5 {
 if atomic.LoadUint32(&done) == 0 {
  mu.Lock()
  defer mu.Unlock()
  if instance == nil {
   defer atomic.StoreUint32(&done, 1)
   instance = new(singletonV5)
  }
 }
 fmt.Printf("%p\n", instance)
 return instance
}

sync/once:

我們在介紹 Go 語言併發的文章中,瞭解到 sync/once 包的 Do() 方法可以確保只執行一次。

type singletonV6 struct {

}

var instance *singletonV6

var once sync.Once

func GetInstance() *singletonV6 {
 once.Do(func() {
  instance = new(singletonV6)
 })
 fmt.Printf("%p\n", instance)
 return instance
}

我們可以通過閱讀 sync/once 包的源碼發現,實際上 Do() 方法也是使用了 sync/atomic 包的 StoreUint32 方法和 LoadUint32() 方法。

05 總結

本文我們介紹了創建型設計模式 - 單例模式,並且介紹了幾種 Go 實現方式。

需要注意的是,我們在高併發場景中,需要考慮併發安全的問題。

參考資料:

  1. https://en.wikipedia.org/wiki/Software_design_pattern

  2. https://en.wikipedia.org/wiki/Design_Patterns

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ixTP1qythuJyhf3b9kIRaA