Golang 單例模式與sync.Once

語言: CN / TW / HK

Golang 單例模式與sync.Once

背景

單例模式可以說是最簡單的設計模式之一了,功能很簡單:一個型別的東西只例項化一次,全域性只有一個例項,並提供方法來獲取該例項。

在 Golang 中變數或說明例項只初始化一次的效果通過init函式是可以實現的,包在被引入時就會執行一次init函式且無論同一包被引入多少次也都只執行一次。

不過本文主要想討論的單例模式是第一次需要用到時才去初始化,也就是延遲初始化。

不太好的單例實現

// bad_singleton.go

package main

import (
    "sync"
)

var svcMu sync.Mutex
var svc *Svc

type Svc struct {
    Num int
}

func GetSvc() *Svc {
    if svc == nil { // 這一步判斷不是併發安全的
        svcMu.Lock()
        defer svcMu.Unlock()
        if svc == nil {
            svc = &Svc{Num: 1}
            svc = &Svc{}
            svc.Num = 1

        }
    }

    return svc
}

注意執行互斥鎖svcMu.Lock()前的語句if svc == nil 並不是併發安全的,即在多個 goroutine 併發呼叫的場景下,其中的一個 goroutine 正在初始化這個變數svc的過程中,這裡別的 goroutine 判斷得到svc不等於nil的結果時也並不意味著svc就一定完成初始化了。

因為在缺乏顯式同步的情況下,編譯器和CPU在能保證每個 goroutine 內滿足序列一致性的基礎上可以自由地重排訪問記憶體的指令順序。

比如svc = &Svc{Num: 1}這行看上去只是一條執行語句,可能重排後的一種實現是像下面這樣的:

svc = &Svc{}
svc.Num = 1

可見,不等於nil並不意味著就一定完成了初始化,因此上面示例是一種不太好的單例實現。

比較好的單例實現

// good_singleton.go

package main

import (
    "sync"
)

var svcOnce sync.Once
var svc *Svc

type Svc struct {
    Num int
}

func GetSvc() *Svc {
    svcOnce.Do(func() {
        svc = &Svc{Num: 1}
    })

    return svc
}

sync.Once提供的Do方法無論被呼叫多少次都只執行傳入的函式一次,那為什麼說直接使用Do方法執行初始化而不是套一層if svc == nil 才是比較好的做法呢,下面結合sync.Once原始碼來說明。

// sync.Once 原始碼

package sync

import (
    "sync/atomic"
)

type Once struct {
    done uint32
    m    Mutex
}

func (o *Once) Do(f func()) {
    if atomic.LoadUint32(&o.done) == 0 { // 這步是判斷是否已經完成初始化的關鍵
        o.doSlow(f)
    }
}

func (o *Once) doSlow(f func()) {
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {
        defer atomic.StoreUint32(&o.done, 1)
        f()
    }
}

官方對於sync.Once的實現是非常短小精悍的。其中atomic.LoadUint32(&o.done) == 0是關鍵的一步,這裡採用的是原子操作語句,保證了即使在併發場景下也是安全的,對資料的讀寫都是完整的。

o.done的值為0時表示未進行初始化或正在初始化中,只有等於1時才表示初始化已經完成,即f()執行完成後由defer atomic.StoreUint32(&o.done, 1)語句給o.done賦值1;也就是o.done作為是否完成初始化的標識,可能的值只有前面說的兩個,為0時則加鎖並嘗試初始化流程,反之則視為已完成初始化直接跳過,這樣就完美兼顧了效率與併發安全。

由此可見sync.Once內建的初始化完成標識判斷遠比if svc == nil 靠譜,因此像上面這樣使用sync.Once實現單例模式是最推薦的方式。

額外推薦

實則開發中用到的設計模式經常不止一種,越是複雜大型的專案就越需要使用更多合適的模式來優化程式碼。

下面要推薦的是RefactoringGuru。這是我所見過最好的設計模式教程,是國外建立的一個教程網站,有中文站點,圖文並茂地介紹每一種模式的結構、關係和邏輯,
最重要的是示例程式碼方面囊括了常見的幾種主流程式語言,是個適合多數程式設計師學習設計模式的好地方!

下圖是設計模式的目錄頁面(是不是很圖文並茂呢):

結語

以上為本人學習和實踐的一些總結,如有錯漏還請不吝賜教。

參考

《Go程式設計語言》9.5 延遲初始化:sync.Once 網路版
Go 單例模式講解和程式碼示例

本文來源: Golang 單例模式與sync.Once