golang使用sync.Once實(shí)現(xiàn)懶加載的用法和坑點(diǎn)詳解
在使用Golang做后端開發(fā)的工程中,我們通常需要聲明一些一些配置類或服務(wù)單例等在業(yè)務(wù)邏輯層面較為底層的實(shí)例。為了節(jié)省內(nèi)存或是冷啟動(dòng)開銷,我們通常采用lazy-load懶加載的方式去初始化這些實(shí)例。初始化單例這個(gè)行為是一個(gè)非常經(jīng)典的并發(fā)處理的案例,比如在java當(dāng)中,我們可能用到建立雙重鎖+volatile的方式保證初始化邏輯只被訪問一次,并且所有線程最終都可以讀取到初始化完成的實(shí)例產(chǎn)物。這段經(jīng)典的代碼可以按如下的方式編寫:
public class Singleton {
private volatile static Singleton uniqueSingleton;
private Singleton() {
}
public Singleton getInstance() {
if (null == uniqueSingleton) {
synchronized (Singleton.class) {
if (null == uniqueSingleton) {
uniqueSingleton = new Singleton();
}
}
}
return uniqueSingleton;
}
}
但在Golang里面,實(shí)現(xiàn)懶加載的方式可以簡(jiǎn)單的多,用內(nèi)置的sync.Once就能滿足。假設(shè)我們有一個(gè)user單例,需要被1000個(gè)線程讀取并打印,就可以這樣子寫:
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
var user *User
var userOnce sync.Once
func initUser() {
user = &User{}
cfgStr := `{"name":"foobar","age":18}`
if err := json.Unmarshal([]byte(cfgStr), user); err != nil {
panic("load user err: " + err.Error())
}
}
func getUser() *User {
userOnce.Do(initUser)
return user
}
func TestSyncOnce(t *testing.T) {
var wg sync.WaitGroup
for i := 1; i < 1000; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
curUser := getUser()
t.Logf("[%d] got user: %+v", n, curUser)
}(i)
}
wg.Wait()
}
這段代碼里,首先是通過var userOnce sync.Once聲明了一個(gè)sync.Once實(shí)例,然后在getUser當(dāng)中,我們聲明了userOnce.Do(initUser)這個(gè)操作。假設(shè)一個(gè)goroutine最先到達(dá)這個(gè)操作,就會(huì)上鎖并執(zhí)行initUser,其它goroutine到達(dá)之后,得等第一個(gè)goroutine執(zhí)行完initUser之后,才會(huì)繼續(xù)return user。這樣,就能一來保證initUser只會(huì)執(zhí)行一次,二來所有g(shù)oroutine都能夠最終讀到初始化完成的user單例。
sync.Once的工作機(jī)理也很簡(jiǎn)單,通過一個(gè)鎖和一個(gè)flag就能夠?qū)崿F(xiàn):
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 { // 如果是1表示已經(jīng)完成了,跳過
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock() // 只有1個(gè)goroutine能拿到鎖,其它的等待
defer o.m.Unlock()
if o.done == 0 { // 如果還是0表示第一個(gè)來的,不是0就表示已經(jīng)有g(shù)oroutine做完了
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
最后也需要注意,sync.Once使用上面有一個(gè)坑點(diǎn),不能也不需要像java一樣為單例提前做nil判斷。比如下面一段代碼是有問題的:
func initUser() {
user = &User{} // 先給一個(gè)zero-value實(shí)例
cfgStr := `{"name":"foobar","age":18}` // 然后加載json內(nèi)容,完成初始化
if err := json.Unmarshal([]byte(cfgStr), user); err != nil {
panic("load user err: " + err.Error())
}
}
func getUser() *User {
if user == nil {
userOnce.Do(initUser)
}
return user
}
由于Golang沒有volatile關(guān)鍵字,不能控制單例在內(nèi)存的可見性,那么多goroutine并發(fā)時(shí),就有可能出現(xiàn)這樣的執(zhí)行時(shí)序:
- goroutine-A過了getUser的user == nil判斷,進(jìn)入到了initUser邏輯,走到了cfgStr := XXX一行
- 此時(shí)切換到goroutine-B,因?yàn)間oroutine-A在initUser已經(jīng)走過了user = &User{}一行,所以跳過了user == nil判斷,直接返回沒有完全初始化的user實(shí)例,然后一直往下運(yùn)行,就沒切回給goroutine-A
這樣的結(jié)果,就導(dǎo)致有g(shù)oroutine拿到未初始化完成的實(shí)例往后運(yùn)行,后面就出問題了。所以實(shí)戰(zhàn)當(dāng)中需要留意,用sync.Once時(shí),不能也不需要加這些nil判斷,就能滿足懶加載單例/配置之類的邏輯。
到此這篇關(guān)于golang使用sync.Once實(shí)現(xiàn)懶加載的用法和坑點(diǎn)詳解的文章就介紹到這了,更多相關(guān)go sync.Once懶加載內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Go語言標(biāo)準(zhǔn)庫sync.Once使用場(chǎng)景及性能優(yōu)化詳解
- golang中sync.Once只執(zhí)行一次的原理解析
- Golang并發(fā)利器sync.Once的用法詳解
- go并發(fā)利器sync.Once使用示例詳解
- go?sync.Once實(shí)現(xiàn)高效單例模式詳解
- Golang基于sync.Once實(shí)現(xiàn)單例的操作代碼
- 一文解析 Golang sync.Once 用法及原理
- Go并發(fā)編程之sync.Once使用實(shí)例詳解
- Go語言并發(fā)編程 sync.Once
- 深入理解go sync.Once的具體使用
相關(guān)文章
我放棄Python轉(zhuǎn)Go語言的9大理由(附優(yōu)秀書籍推薦)
這篇文章主要給大家介紹了關(guān)于我放棄Python轉(zhuǎn)Go語言的9大理由,以及給大家推薦了6本優(yōu)秀的go語言書籍,對(duì)同樣想學(xué)習(xí)golang的朋友們具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-10-10
幾個(gè)小技巧幫你實(shí)現(xiàn)Golang永久阻塞
Go 的運(yùn)行時(shí)的當(dāng)前設(shè)計(jì),假定程序員自己負(fù)責(zé)檢測(cè)何時(shí)終止一個(gè) goroutine 以及何時(shí)終止該程序。有時(shí)候我們需要的是使程序阻塞在這一行,本文就來詳細(xì)的介紹一下,感興趣的可以了解一下2021-12-12
Golang實(shí)現(xiàn)不被復(fù)制的結(jié)構(gòu)體的方法
sync包中的許多結(jié)構(gòu)都是不允許拷貝的,因?yàn)樗鼈冏陨泶鎯?chǔ)了一些狀態(tài)(比如等待者的數(shù)量),如果你嘗試復(fù)制這些結(jié)構(gòu)體,就會(huì)在你的?IDE中看到警告,那這是怎么實(shí)現(xiàn)的呢,下文就來和大家詳細(xì)講講2023-03-03
Go?結(jié)構(gòu)體序列化的實(shí)現(xiàn)
本文主要介紹了Go?結(jié)構(gòu)體序列化的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01
使用gRPC實(shí)現(xiàn)獲取數(shù)據(jù)庫版本
這篇文章主要為大家詳細(xì)介紹了如何使用gRPC實(shí)現(xiàn)獲取數(shù)據(jù)庫版本,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-12-12
golang elasticsearch Client的使用詳解
這篇文章主要介紹了golang elasticsearch Client的使用詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-05-05
go語言VScode?see?'go?help?modules'?(exit?statu
最近上手學(xué)習(xí)go語言,準(zhǔn)備在VSCode上寫程序的時(shí)候卻發(fā)現(xiàn)出了一點(diǎn)問題,下面這篇文章主要給大家介紹了關(guān)于go語言VScode?see?'go?help?modules'(exit?status?1)問題的解決過程,需要的朋友可以參考下2022-07-07
詳解Go語言如何使用標(biāo)準(zhǔn)庫sort對(duì)切片進(jìn)行排序
Sort?標(biāo)準(zhǔn)庫提供了對(duì)基本數(shù)據(jù)類型的切片和自定義類型的切片進(jìn)行排序的函數(shù)。今天主要分享的內(nèi)容是使用?Go?標(biāo)準(zhǔn)庫?sort?對(duì)切片進(jìn)行排序,感興趣的可以了解一下2022-12-12

