golang設計模式之單例模式

silsuer在掘金發表於2018-11-02

單例模式

wiki百科: 單例模式,也叫單子模式,是一種常用的軟體設計模式。在應用這個模式時,單例物件的類必須保證只有一個例項存在。許多時候整個系統只需要擁有一個的全域性物件,這樣有利於我們協調系統整體的行為。比如在某個伺服器程式中,該伺服器的配置資訊存放在一個檔案中,這些配置資料由一個單例物件統一讀取,然後服務程式中的其他物件再通過這個單例物件獲取這些配置資訊。這種方式簡化了在複雜環境下的配置管理。

單例模式要實現的效果就是,對於應用單例模式的類,整個程式中只存在一個例項化物件

go並不是一種物件導向的語言,所以我們使用結構體來替代

有幾種方式:

  • 懶漢模式

  • 餓漢模式

  • 雙重檢查鎖機制

下面拆分講解:

懶漢模式

  1. 構建一個示例結構體
   type example struct {
   	name string
   }
複製程式碼
  1. 設定一個私有變數作為每次要返回的單例
  var instance *example
複製程式碼
  1. 寫一個可以獲取單例的方法
    func GetExample() *example {
    
    	// 存線上程安全問題,高併發時有可能建立多個物件
    	if instance == nil {
    		instance = new(example)
    	}
    	return instance
    }
複製程式碼
  1. 測試一下

      func main() {
      	s := GetExample()
      	s.name = "第一次賦值單例模式"
      	fmt.Println(s.name)
      
      	s2 := GetExample()
      	fmt.Println(s2.name)
      }
    複製程式碼

懶漢模式存線上程安全問題,在第3步的時候,如果有多個執行緒同時呼叫了這個方法, 那麼都會檢測到instancenil,就會建立多個物件,所以出現了餓漢模式...

餓漢模式

與懶漢模式類似,不再多說,直接上程式碼


  // 構建一個結構體,用來例項化單例
  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.OnceDo方法可以實現在程式執行過程中只執行一次其中的回撥,所以最終簡化的程式碼如下:


 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)
 }
複製程式碼

單例模式是開發中經常用到的設計模式,我在製作自己的web框架 silsuer/bingo 的時候 在環境變數控制、配置項控制等位置都用到了這種模式。

想把所有設計模式使用golang實現一遍,開了個新坑silsuer/golang-design-patterns, 這是第一篇,以後會陸續更新,需要請自取~

此文章的原始碼都在這個倉庫中: golang設計模式

打個廣告,推薦一下自己寫的 go web框架 bingo,求star,求PR ~

相關文章