最簡單的單例模式,Go 版本的實現你寫對了嗎?

大家好,我是網管,首先我問大家一個問題,你們面試的時候,面試官有沒有問過你們:"你都用過什麼設計模式?",我猜多數人的回答會把單例模式,放在第一位。

我:"呃… 我用過單例、工廠、觀察者,反向代理,裝飾器,哨兵"…. ",

面試官內心 OS:"我都沒用過這麼多... 反向代理是什麼鬼,這小子背串了吧,不管了先就坡下驢,從頭開始問"。

面試官:"用過的挺多哈,那麼你能說下單例你都在什麼情況下用,順便在這張紙上實現一下單例吧"。

我:"當需要確保一個類型,只有一個實例時就需要使用單例模式了"。

面試官:"好,那你在紙上實現一下"

十分鐘後的我:"不好意思,我們之前項目裏都封裝好了,我只用過,沒有機會實現,所以..."

面試官內心 OS:"好吧,這個面試 KPI 要求得進行三十分鐘,這還有小二十分鐘呢,隨便再問問,就讓他回去等信兒吧"

面試卒...

上面是我給大家編的一個場景,如有雷同,請憋住,不要在工位上笑噴~。單例模式雖然簡單,不過還是有一些說道兒的,一是應用比較廣泛,再來如果不注意容易在多線程環境下造成 BUG,今天就給大家簡單說下單例模式的應用,以及用 Go 語言怎麼正確地實現單例模式。

單例模式

上面對話裏說的沒錯,單例模式是用來控制類型實例的數量的,當需要確保一個類型只有一個實例時,就需要使用單例模式。

由於要控制數量,那麼可想而之只能把實例的訪問進行收口,不能誰來了都能 new 一個出來,所以單例模式還會提供一個訪問該實例的全局端口,一般都會命名個 GetInstance之類的函數用作實例訪問的端口。

又因爲在什麼時間創建出實例,單例模式又可以分裂出餓漢模式 和 懶漢模式,前者適用於在程序早期初始化時創建已經確定需要加載的類型實例,比如項目的數據庫實例。後者其實就是延遲加載的模式,適合程序執行過程中條件成立才創建加載的類型實例。

下面我們用 Go 代碼把這兩種單例模式實現一下。

餓漢模式

這個模式用 Go 語言實現時,藉助 Go 的init函數來實現特別方便

如果你想了解 Go init 函數的方方面面,可以看以前的老文章 Go 語言 init 函數你必須記住的六個特徵

下面用單例模式返回數據庫連接實例,相信你們在項目裏都見過類似代碼。

package dao
// 餓漢式單例
// 注意定義非導出類型
type  databaseConn struct{
  ...
}

var dbConn *databaseConn

func init() {
  dbConn = &databaseConn{}
}

// GetInstance 獲取實例
func Db() *databaseConn {
 return dbConn
}

這裏初始化數據庫的細節咱們就不多費文筆了,實際情況肯定是從配置中心加載下來數據庫連接配置再實例化數據庫的連接對象。這裏有人可能會有個問題,你這一個程序進程就只有一個數據連接實例,那這麼多請求都用一個數據庫連接行嗎?

誒,這個是對數據庫連接的抽象呀,這個實例會維護一個連接池,那裏纔是真正去請求數據庫用的連接。是不是有點暈,有點暈去看看你們項目裏這塊的代碼。一般會看到初始化實例時,讓你設置最大連接數、閒置連接數和存活時間這樣的連接池配置。

懶漢模式

懶漢模式 -- 通俗點說就是延遲加載,不過這塊特別注意,要考慮併發環境下,你的判斷實例是否已經創建時,是不是用的當前讀。在一些教設計模式的教程裏,一般這種情況下會舉一個例子 -- 用 Java 雙重鎖實現線程安全的單例模式,雙重鎖指的是volatilesynchronized

class Singleton {
    private volatile static Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

上面這個例子裏,如果不給instance屬性加上 volatile修飾符,那麼雖說創建的過程已經用synchronized給類加了鎖,但是有可能讀到的instance是線程緩存是滯後的,有可能屬性此時已經被其他線程初始化了,所以就必須加上volatile保證當前讀(讀主存裏屬性的狀態)。

那麼 Go 裏邊沒有volatile這種機制,我們該怎麼辦呢?聰明的你一定能想得出,我們定義一個實例的狀態變量,然後用原子操作atomic.Loadatomic.Store去讀寫這個狀態變量,不就是實現了嗎?像下面這樣:

如果 Go 原子操作你還不熟,請看老文章 Golang 五種原子性操作的用法詳解

import "sync"
import "sync/atomic"

var initialized uint32

type singleton struct {
  ...
}

func GetInstance() *singleton {

    if atomic.LoadUInt32(&initialized) == 1 {  // 原子操作 
      return instance
   }

    mu.Lock()
    defer mu.Unlock()

    if initialized == 0 {
         instance = &singleton{}
         atomic.StoreUint32(&initialized, 1)
    }

    return instance
}

確實,相當於把上面 Java 的例子翻譯成用 Go 實現了,不過還有另外一種更Go native 的寫法,比這種寫法更簡練。如果用 Go 更慣用的寫法,我們可以藉助其sync庫中自帶的併發同步原語Once來實現:

package singleton

import (
    "sync"
)

type singleton struct {}

var instance *singleton
var once sync.Once

func GetInstance() *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}

關於sync.One 的使用和其實現原理… 我發現我的 Go 併發編程系列裏沒單獨寫Once這個原語,可能是覺得太簡單了吧,後期抽空補上吧… 不過只是原理分析沒寫,怎麼應用在 Go 語言 sync 包的應用詳解裏也能找到。

總結

這篇文章其實是把單例模式的應用,和 Go 的單例模式版本怎麼實現給大家說了一下,現在教程大部分都是用 Java 講設計模式的,雖然我們可以直接翻譯,不過有的時候 Go 有些更 native 的實現方式,讓實現更簡約一些。之前還寫了一個觀察者模式的文章,拒絕 Go 代碼臃腫,其實在這幾塊可以用下觀察者模式

我是不會立用 Go 學設計模式這個系列的 flag 的,所以每個標題都會更具體化一些,萬一,我說萬一,哪天真的把這些設計模式湊齊了,再給大家發個合集通知。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/1ZuhUA9Lt2uLFlamIY6fLQ