GoLang設計模式04 - 單例模式

robin·張發表於2021-09-10

單例模式恐怕是最為人熟知的一種設計模式了。它同樣也是建立型模式的一種。當某個struct只允許有一個例項的時候,我們會用到這種設計模式。這個struct的唯一的例項被稱為單例物件。下面是需要建立單例物件的一些場景:

  • 資料庫例項:一般在開發中,對於一個應用,我們通常只需要一個資料庫物件例項
  • 日誌例項:同樣,對於一個應用來說,日誌操作物件也只需要一個例項

單例物件通常在struct初始化的時候建立。通常,如果某個struct只需要建立一個例項的時候,會為其定義一個getInstance()方法,建立的單例例項會通過這個方法返回給呼叫者。

因為Go語言中有goroutines,它會給單例模式的應用帶來一些麻煩。我們在構建單例模式的時候必須要考慮到在多個goroutines訪問struct的getInstance()方法的時候應該返回相同的例項。下面的程式碼演示瞭如何正確的建立一個單例物件:

var lock = &sync.Mutex{}

type single struct {
}

var singleInstance *single

func getInstance() *single {
    if singleInstance == nil {
        lock.Lock()
        defer lock.Unlock()
        if singleInstance == nil {
            fmt.Println("Creting Single Instance Now")
            singleInstance = &single{}
        } else {
            fmt.Println("Single Instance already created-1")
        }
    } else {
        fmt.Println("Single Instance already created-2")
    }
    return singleInstance
}

以上的程式碼保證了single struct只會有一個例項。程式碼中有幾處可以注意下:

  1. getInstance()方法的起始處首先檢查了下singleInstance是否為nil。這樣每次呼叫getInstance()方法的時候可以避免執行“鎖”操作。因為“鎖”相關的操作比較耗資源,會影響效能,因此越少呼叫越好。
  2. singleInstance物件在“鎖”作用區間內建立,可以避免goroutines的影響。
  3. 在獲取到“鎖”資源後,程式中又一次校驗了singleInstance物件是否為空。這是因為可能會有多個goroutines通過第一次校驗,二次校驗可以保證只有一個goroutine建立單例,不然每個goroutine都有可能會建立一個single struct例項。

完整程式碼在這裡:

single.go

import (
	"fmt"
	"sync"
)

var lock = &sync.Mutex{}

type single struct {
}

var singleInstance *single

func GetInstance() *single {
	if singleInstance == nil {
		lock.Lock()
		defer lock.Unlock()
		if singleInstance == nil {
			fmt.Println("Creating Single Instance Now")
			singleInstance = &single{}
		} else {
			fmt.Println("Single Instance already created-1")
		}
	} else {
		fmt.Println("Single Instance already created-2")
	}
	return singleInstance
}

  main.go

import (
	"fmt"
)

func main() {
	for i := 0; i < 100; i++ {
		go GetInstance()
	}
	// Scanln is similar to Scan, but stops scanning at a newline and
	// after the final item there must be a newline or EOF.
	fmt.Scanln()
}

  輸出內容:

Creating Single Instance Now
Single Instance already created-1
Single Instance already created-2
Single Instance already created-1
Single Instance already created-1
Single Instance already created-1
Single Instance already created-1
Single Instance already created-1
Single Instance already created-1
Single Instance already created-2
Single Instance already created-2
...

簡單說明下:

  • 輸出內容中只有一行“Creating Single Instance Now”這樣的輸出,這說明只有一個goroutine能夠建立一個singlestruct的例項。
  • 輸出內容中有多行“Single Instance already created-1”,說明有多個goroutines通過第一次檢查singleInstance物件是否為空的校驗,它本來都有機會建立單例。
  • 最後輸出的都是“Single Instance already created-2”,意味著單例已建立完成,之後的goroutines都無法再通過首次校驗。

除了鎖+二次校驗的方式,還有其它建立單例的方法,我們來看一下:

基於init()函式

init()函式中建立單例。因為一個包中每個檔案的init()函式都只會呼叫一次,這樣就可以保證只有一個例項會被建立。看下程式碼:

import (
	"fmt"
	"log"
)

type single struct {
}

var singleInstance *single

func init() {
	fmt.Println("Creating Single Instance Now")
	singleInstance = &single{}
}

func GetInstance() *single {
	if singleInstance == nil {
		log.Fatal("Single Instance is nil")
	} else {
		fmt.Println("Single Instance already created-2")
	}
	return singleInstance
}

這應該就是go語言中的懶漢式單例建立方法了。如果不介意過早建立例項造成的資源佔用,推薦使用這種方法建立單例。

通過sync.Once

sync.Once中的程式碼會被保證只執行一次,這完全可以用來建立單例。程式碼如下:

import (
	"fmt"
	"sync"
)

var once sync.Once

type single struct {
}

var singleInstance *single

func GetInstance() *single {
	if singleInstance == nil {
		once.Do(
			func() {
				fmt.Println("Creating Single Instance Now")
				singleInstance = &single{}
			})
		fmt.Println("Single Instance already created-1")
	} else {
		fmt.Println("Single Instance already created-2")
	}
	return singleInstance
}

相比二次校驗的方式,這裡的程式碼可以說非常簡潔了。這也是我非常建議使用的一種單例建立方式。

輸出內容為:

Creating Single Instance Now
Single Instance already created-1
Single Instance already created-2
Single Instance already created-2
Single Instance already created-1
Single Instance already created-2
Single Instance already created-1
Single Instance already created-1
Single Instance already created-2
Single Instance already created-1
Single Instance already created-2
Single Instance already created-1
Single Instance already created-2
Single Instance already created-2
Single Instance already created-2
Single Instance already created-2
Single Instance already created-2
Single Instance already created-2
Single Instance already created-2
Single Instance already created-2
...

簡單說明下: 

  • 輸出的內容和二次校驗的方式差不多,仍然存在多行“Single Instance already created-1”這樣的輸出,說明有多個goroutine通過了if校驗
  • 輸出內容中只有一行“Creating Single Instance Now”,說明只有一個goroutine能夠建立例項。

程式碼已上傳至GitHub:zhyea / go-patterns / singleton-pattern

End!

相關文章