1. 程式人生 > >設計模式-單例模式(Go語言描述)

設計模式-單例模式(Go語言描述)

這篇部落格我們繼續來看設計模式,今天帶來的是一個最簡單而且最常用的模式-單例模式。那什麼是單例模式呢?相信大家最它最熟悉不過了,那我們就來快速的瞭解一下它的定義。

保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點。

這個解釋足夠簡單。說白了就是假如我們希望我們在我們的系統中該類僅僅存在1個或0個該類的例項。雖然單例模式很簡單,但是熟悉java的同學可能瞭解,單例模式有很多寫法,懶漢式餓漢式雙重鎖。。。 這麼多形式,難道有什麼目的?確實,不過他們的目的很明確,就是保證在一種特殊情況下的單例-併發

ok,既然瞭解了單例模式,那下面我們就開始用程式碼描述一下單例模式。首先是最簡單的單例,這裡我們並不去考慮併發的情況

package manager
import (
    "fmt"
)

var m *Manager

func GetInstance() *Manager {
    if m == nil {
        m = &Manager {}
    }
    return m
}

type Manager struct {}

func (p Manager) Manage() {
    fmt.Println("manage...")
}

這就是一個最簡單的單例了,對於Manager結構體,我們提供了一個GetInstance函式去獲取它的例項,這個函式中首先去判斷m變數是否為空,如果為空才去賦值一個Manager

的指標型別的值,一個小小的判斷,就保證了我們在第第二次呼叫GetInstance的時候直接返回m,而不是重新獲取Manager的例項,進而保證了唯一例項。

上面的程式碼確實簡單,也實現了最簡單的單例模式,不過大家有沒有考慮到併發這一點,在併發的情況下,這裡是不是還可以正常工作呢? 來,先跟著下面的思路走一走,來看看問題出現在哪。

現在我們是在併發的情況下去呼叫的 GetInstance函式,現在恰好第一個goroutine執行到m = &Manager {}這句話之前,第二個goroutine也來獲取例項了,第二個goroutine去判斷m是不是nil,因為m = &Manager{}

還沒有來得及執行,所以m肯定是nil,現在出現的問題就是if中的語句可能會執行兩遍!

在上面介紹的這種情形中,因為m = &Manager{}可能會執行多次,所以我們寫的單例失效了,這個時候我們就該考慮為我們的單例加鎖啦。

這個時候我們就需要引入go的鎖機制-sync.Mutex了,修改我們的程式碼,

package manager
import (
    "sync"
    "fmt"
)

var m *Manager
var lock *sync.Mutex = &sync.Mutex {}

func GetInstance() *Manager {
    lock.Lock()
    defer lock.Unlock()
    if m == nil {
        m = &Manager {}
    }
    return m
}

type Manager struct {}

func (p Manager) Manage() {
    fmt.Println("manage...")
}

程式碼做了簡單的修改了,引入了鎖的機制,在GetInstance函式中,每次呼叫我們都會上一把鎖,保證只有一個goroutine執行它,這個時候併發的問題就解決了。不過現在不管什麼情況下都會上一把鎖,而且加鎖的代價是很大的,有沒有辦法繼續對我們的程式碼進行進一步的優化呢? 熟悉java的同學可能早就想到了雙重的概念,沒錯,在go中我們也可以使用雙重鎖機制來提高效率。

package manager
import (
    "sync"
    "fmt"
)

var m *Manager
var lock *sync.Mutex = &sync.Mutex {}

func GetInstance() *Manager {
    if m == nil {
        lock.Lock()
        defer lock.Unlock()
        if m == nil {
            m = &Manager {}
        }
    }

    return m
}

type Manager struct {}

func (p Manager) Manage() {
    fmt.Println("manage...")
}

程式碼只是稍作修改而已,不過我們用了兩個判斷,而且我們將同步鎖放在了條件判斷之後,這樣做就避免了每次呼叫都加鎖,提高了程式碼的執行效率。

這獲取就是很完美的單例程式碼了,不過還沒完,在go中我們還有更優雅的方式去實現。單例的目的是啥?保證例項化的程式碼只執行一次,在go中就中這麼一種機制來保證程式碼只執行一次,而且不需要我們手工去加鎖解鎖。對,就是我們的sync.Once,它有一個Do方法,在它中的函式go會只保證僅僅呼叫一次!再次修改我們的程式碼,

package manager
import (
    "sync"
    "fmt"
)

var m *Manager
var once sync.Once

func GetInstance() *Manager {
    once.Do(func() {
        m = &Manager {}
    })
    return m
}

type Manager struct {}

func (p Manager) Manage() {
    fmt.Println("manage...")
}

程式碼更簡單了,而且有沒有發現-漂亮了!Once.Do方法的引數是一個函式,這裡我們給的是一個匿名函式,在這個函式中我們做的工作很簡單,就是去賦值m變數,而且go能保證這個函式中的程式碼僅僅執行一次!

ok,到現在單例模式我們就介紹完了,內容並不多,因為單例模式太簡單而且太常見了。我們用單例的目的是為了保證在整個系統中存在唯一的例項,我們加鎖的目的是為了在併發的環境中單例依舊好用。不過雖然單例簡單,我們還是不能任性的用,因為這樣做例項會一直存在記憶體中,一些我們用的不是那麼頻繁的東西使用了單例是不是就造成了記憶體的浪費?大家在用單例的時候還是要多思考思考,這個模組適不適合用單例!