簡(jiǎn)單的單例模式,Go版本的實(shí)現(xiàn)你寫對(duì)了嗎?
大家好,我是網(wǎng)管,首先我問(wèn)大家一個(gè)問(wèn)題,你們面試的時(shí)候,面試官有沒(méi)有問(wèn)過(guò)你們:"你都用過(guò)什么設(shè)計(jì)模式?",我猜多數(shù)人的回答會(huì)把單例模式,放在第一位。
我:"呃… 我用過(guò)單例、工廠、觀察者,反向代理,裝飾器,哨兵"…. ",
面試官內(nèi)心OS:"我都沒(méi)用過(guò)這么多...反向代理是什么鬼,這小子背串了吧,不管了先就坡下驢,從頭開始問(wèn)"。
面試官:"用過(guò)的挺多哈,那么你能說(shuō)下單例你都在什么情況下用,順便在這張紙上實(shí)現(xiàn)一下單例吧"。
我:"當(dāng)需要確保一個(gè)類型,只有一個(gè)實(shí)例時(shí)就需要使用單例模式了"。
面試官:"好,那你在紙上實(shí)現(xiàn)一下"
十分鐘后的我:"不好意思,我們之前項(xiàng)目里都封裝好了,我只用過(guò),沒(méi)有機(jī)會(huì)實(shí)現(xiàn),所以..."
面試官內(nèi)心OS:"好吧,這個(gè)面試KPI要求得進(jìn)行三十分鐘,這還有小二十分鐘呢,隨便再問(wèn)問(wèn),就讓他回去等信兒吧"
面試卒...
上面是我給大家編的一個(gè)場(chǎng)景,如有雷同,請(qǐng)憋住,不要在工位上笑噴~。單例模式雖然簡(jiǎn)單,不過(guò)還是有一些說(shuō)道兒的,一是應(yīng)用比較廣泛,再來(lái)如果不注意容易在多線程環(huán)境下造成BUG,今天就給大家簡(jiǎn)單說(shuō)下單例模式的應(yīng)用,以及用Go語(yǔ)言怎么正確地實(shí)現(xiàn)單例模式。
單例模式
上面對(duì)話里說(shuō)的沒(méi)錯(cuò),單例模式是用來(lái)控制類型實(shí)例的數(shù)量的,當(dāng)需要確保一個(gè)類型只有一個(gè)實(shí)例時(shí),就需要使用單例模式。
由于要控制數(shù)量,那么可想而之只能把實(shí)例的訪問(wèn)進(jìn)行收口,不能誰(shuí)來(lái)了都能 new 一個(gè)出來(lái),所以單例模式還會(huì)提供一個(gè)訪問(wèn)該實(shí)例的全局端口,一般都會(huì)命名個(gè) GetInstance之類的函數(shù)用作實(shí)例訪問(wèn)的端口。
又因?yàn)樵谑裁磿r(shí)間創(chuàng)建出實(shí)例,單例模式又可以分裂出餓漢模式? 和 懶漢模式,前者適用于在程序早期初始化時(shí)創(chuàng)建已經(jīng)確定需要加載的類型實(shí)例,比如項(xiàng)目的數(shù)據(jù)庫(kù)實(shí)例。后者其實(shí)就是延遲加載的模式,適合程序執(zhí)行過(guò)程中條件成立才創(chuàng)建加載的類型實(shí)例。
下面我們用 Go 代碼把這兩種單例模式實(shí)現(xiàn)一下。
餓漢模式
這個(gè)模式用 Go 語(yǔ)言實(shí)現(xiàn)時(shí),借助 Go 的init函數(shù)來(lái)實(shí)現(xiàn)特別方便
如果你想了解 Go init 函數(shù)的方方面面,可以看以前的老文章Go語(yǔ)言init函數(shù)你必須記住的六個(gè)特征
下面用單例模式返回?cái)?shù)據(jù)庫(kù)連接實(shí)例,相信你們?cè)陧?xiàng)目里都見過(guò)類似代碼。
package dao
// 餓漢式單例
// 注意定義非導(dǎo)出類型
type databaseConn struct{
...
}
var dbConn *databaseConn
func init() {
dbConn = &databaseConn{}
}
// GetInstance 獲取實(shí)例
func Db() *databaseConn {
return dbConn
}
這里初始化數(shù)據(jù)庫(kù)的細(xì)節(jié)咱們就不多費(fèi)文筆了,實(shí)際情況肯定是從配置中心加載下來(lái)數(shù)據(jù)庫(kù)連接配置再實(shí)例化數(shù)據(jù)庫(kù)的連接對(duì)象。這里有人可能會(huì)有個(gè)問(wèn)題,你這一個(gè)程序進(jìn)程就只有一個(gè)數(shù)據(jù)連接實(shí)例,那這么多請(qǐng)求都用一個(gè)數(shù)據(jù)庫(kù)連接行嗎?
誒,這個(gè)是對(duì)數(shù)據(jù)庫(kù)連接的抽象呀,這個(gè)實(shí)例會(huì)維護(hù)一個(gè)連接池,那里才是真正去請(qǐng)求數(shù)據(jù)庫(kù)用的連接。是不是有點(diǎn)暈,有點(diǎn)暈去看看你們項(xiàng)目里這塊的代碼。一般會(huì)看到初始化實(shí)例時(shí),讓你設(shè)置最大連接數(shù)、閑置連接數(shù)和存活時(shí)間這樣的連接池配置。
懶漢模式
懶漢模式--通俗點(diǎn)說(shuō)就是延遲加載,不過(guò)這塊特別注意,要考慮并發(fā)環(huán)境下,你的判斷實(shí)例是否已經(jīng)創(chuàng)建時(shí),是不是用的當(dāng)前讀。在一些教設(shè)計(jì)模式的教程里,一般這種情況下會(huì)舉一個(gè)例子--用 Java 雙重鎖實(shí)現(xiàn)線程安全的單例模式,雙重鎖指的是volatile和synchronized。
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;
}
}
上面這個(gè)例子里,如果不給instance?屬性加上 volatile?修飾符,那么雖說(shuō)創(chuàng)建的過(guò)程已經(jīng)用synchronized?給類加了鎖,但是有可能讀到的instance?是線程緩存是滯后的,有可能屬性此時(shí)已經(jīng)被其他線程初始化了,所以就必須加上volatile保證當(dāng)前讀(讀主存里屬性的狀態(tài))。
那么 Go 里邊沒(méi)有volatile?這種機(jī)制,我們?cè)撛趺崔k呢?聰明的你一定能想得出,我們定義一個(gè)實(shí)例的狀態(tài)變量,然后用原子操作atomic.Load、atomic.Store去讀寫這個(gè)狀態(tài)變量,不就是實(shí)現(xiàn)了嗎?像下面這樣:
如果 Go 原子操作你還不熟,請(qǐng)看老文章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
}
確實(shí),相當(dāng)于把上面 Java 的例子翻譯成用 Go 實(shí)現(xiàn)了,不過(guò)還有另外一種更Go? native 的寫法,比這種寫法更簡(jiǎn)練。如果用 Go 更慣用的寫法,我們可以借助其sync?庫(kù)中自帶的并發(fā)同步原語(yǔ)Once來(lái)實(shí)現(xiàn):
package singleton
import (
"sync"
)
type singleton struct {}
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
關(guān)于sync.One ?的使用和其實(shí)現(xiàn)原理…我發(fā)現(xiàn)我的Go 并發(fā)編程系列?里沒(méi)單獨(dú)寫Once?這個(gè)原語(yǔ),可能是覺得太簡(jiǎn)單了吧,后期抽空補(bǔ)上吧… 不過(guò)只是原理分析沒(méi)寫,怎么應(yīng)用在Go語(yǔ)言sync包的應(yīng)用詳解里也能找到。
總結(jié)
這篇文章其實(shí)是把單例模式的應(yīng)用,和Go的單例模式版本怎么實(shí)現(xiàn)給大家說(shuō)了一下,現(xiàn)在教程大部分都是用 Java 講設(shè)計(jì)模式的,雖然我們可以直接翻譯,不過(guò)有的時(shí)候 Go 有些更native 的實(shí)現(xiàn)方式,讓實(shí)現(xiàn)更簡(jiǎn)約一些。