wiki百科: 單例模式,也叫單子模式,是一種常用的軟件設計模式。在應用這個模式時,單例對象的類必須保證只有一個實例存在。許多時候整個系統只需要擁有一個的全局對象,這樣有利於我們協調系統整體的行為。比如在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,然后服務進程中的其他對象再通過這個單例對象獲取這些配置信息。這種方式簡化了在復雜環境下的配置管理。
單例模式要實現的效果就是,對於應用單例模式的類,整個程序中只存在一個實例化對象
go並不是一種面向對象的語言,所以我們使用結構體來替代
有幾種方式:
-
懶漢模式
-
餓漢模式
-
雙重檢查鎖機制
下面拆分講解:
懶漢模式
- 構建一個示例結構體
type example struct { name string }
- 設置一個私有變量作為每次要返回的單例
var instance *example
- 寫一個可以獲取單例的方法
func GetExample() *example { // 存在線程安全問題,高並發時有可能創建多個對象 if instance == nil { instance = new(example) } return instance }
-
測試一下
func main() { s := GetExample() s.name = "第一次賦值單例模式" fmt.Println(s.name) s2 := GetExample() fmt.Println(s2.name) }
懶漢模式存在線程安全問題,在第3步的時候,如果有多個線程同時調用了這個方法, 那么都會檢測到instance
為nil
,就會創建多個對象,所以出現了餓漢模式...
餓漢模式
與懶漢模式類似,不再多說,直接上代碼
// 構建一個結構體,用來實例化單例 type example2 struct { name string } // 聲明一個私有變量,作為單例 var instance2 *example2 // init函數將在包初始化時執行,實例化單例 func init() { instance2 = new(example2) instance2.name = "初始化單例模式" } func GetInstance2() *example2 { return instance2 } func main() { s := GetInstance2() fmt.Println(s.name) }
餓漢模式將在包加載的時候就創建單例對象,當程序中用不到該對象時,浪費了一部分空間
和懶漢模式相比,更安全,但是會減慢程序啟動速度
雙重檢查機制
懶漢模式存在線程安全問題,一般我們使用互斥鎖來解決有可能出現的數據不一致問題
所以修改上面的GetInstance()
方法如下:
var mux Sync.Mutex func GetInstance() *example { mux.Lock() defer mux.Unlock() if instance == nil { instance = &example{} } return instance }
如果這樣去做,每一次請求單例的時候,都會加鎖和減鎖,而鎖的用處只在於解決對象初始化的時候可能出現的並發問題 當對象被創建之后,加鎖就失去了意義,會拖慢速度,所以我們就引入了雙重檢查機制(Check-lock-Check
), 也叫DCL
(Double Check Lock
), 代碼如下:
func GetInstance() *example { if instance == nil { // 單例沒被實例化,才會加鎖 mux.Lock() defer mux.Unlock() if instance == nil { // 單例沒被實例化才會創建 instance = &example{} } } return instance }
這樣只有當對象未初始化的時候,才會又加鎖和減鎖的操作
但是又出現了另一個問題:每一次訪問都要檢查兩次,為了解決這個問題,我們可以使用golang標准包中的方法進行原子性操作:
import "sync" import "sync/atomic" var initialized uint32 func GetInstance() *example { // 一次判斷即可返回 if atomic.LoadUInt32(&initialized) == 1 { return instance } mux.Lock() defer mux.Unlock() if initialized == 0 { instance = &example{} atomic.StoreUint32(&initialized, 1) // 原子裝載 } return instance }
以上代碼只需要經過一次判斷即可返回單例,但是golang標准包中其實給我們提供了相關的方法:
sync.Once
的Do
方法可以實現在程序運行過程中只運行一次其中的回調,所以最終簡化的代碼如下:
type example3 struct { name string } var instance3 *example3 var once sync.Once func GetInstance3() *example3 { once.Do(func() { instance3 = new(example3) instance3.name = "第一次賦值單例" }) return instance3 } func main() { e1 := GetInstance3() fmt.Println(e1.name) e2 := GetInstance3() fmt.Println(e2.name) }