源碼剖析Golang中singleflight的應(yīng)用
前言
前面的一篇文章 Go singleflight:防緩存擊穿利器 詳細(xì)介紹 singleflight
包的使用,展示如何利用它來避免緩存擊穿。而本篇文章,我們來剖析 singleflight
包的源碼實現(xiàn)和工作原理,探索單飛的奧秘。
singleflight 版本:
golang.org/x/sync v0.6.0
結(jié)構(gòu)體解析
Group
Group
是 singleflight
包的一個核心結(jié)構(gòu)體,它管理著所有的請求,確保同一時刻,對同一資源的請求只會被執(zhí)行一次。該結(jié)構(gòu)體的源碼如下所示:
// Group represents a class of work and forms a namespace in // which units of work can be executed with duplicate suppression. type Group struct { mu sync.Mutex // protects m m map[string]*call // lazily initialized }
Group
結(jié)構(gòu)體有兩個字段:
mu sync.Mutex
:一個互斥鎖,用于保護(hù)下面的m
映射。在并發(fā)環(huán)境下,多個goroutine
可能會同時對m
進(jìn)行讀寫操作,所以需要通過互斥鎖來確保對m
的操作是安全的。m map[string]*call
:一個map
映射,其鍵是字符串,值是指向call
結(jié)構(gòu)體的指針。Do
和DoCHan
方法的參數(shù)里,其中一個是key
,m
的鍵保存的就是這個key
。m
是惰性初始化的,意味著它在第一次使用時才會被創(chuàng)建。
Group
通過維護(hù) m
字段來跟蹤每個 key
的調(diào)用狀態(tài),從而實現(xiàn)將多個請求合并成一個請求,多個請求共享同一個結(jié)果。
call
call
結(jié)構(gòu)體表示一個針對特定 key
的正在進(jìn)行中或者已完成的請求,它確保所有同時對該key
調(diào)用 Do
或 DoChan
方法的 goroutine
共享同一個執(zhí)行結(jié)果。該結(jié)構(gòu)體的源碼如下所示:
// call is an in-flight or completed singleflight.Do call type call struct { wg sync.WaitGroup // These fields are written once before the WaitGroup is done // and are only read after the WaitGroup is done. val interface{} err error // These fields are read and written with the singleflight // mutex held before the WaitGroup is done, and are read but // not written after the WaitGroup is done. dups int chans []chan<- Result }
call
結(jié)構(gòu)體有五個字段:
wg sync.WaitGroup
:一個等待組,用于等待當(dāng)前call
的完成。當(dāng)調(diào)用Do
或DoChan
方法后,內(nèi)部會增加WaitGroup
的計數(shù)器,當(dāng)調(diào)用完成后,會減少計數(shù)器。在調(diào)用完成之前,其他想要獲取當(dāng)前call
的結(jié)果的goroutine
會等待WaitGroup
的完成。val interface{}
:調(diào)用Do
或DoChan
方法的結(jié)果值之一,對應(yīng)著fn
函數(shù)(Do
或DoChan
方法的參數(shù))的第一個返回值val
。這個字段在WaitGroup
完成之前被寫入一次,只有在WaitGroup
完成后才會被讀取。err error
:這是在調(diào)用Do
或者DoChan
方法時可能發(fā)生的錯誤。和val
類似,這個字段在WaitGroup
完成之前被寫入一次,只有在WaitGroup
完成后才會被讀取。dups int
:用于計數(shù)當(dāng)前call
的重復(fù)調(diào)用數(shù)量。這個計數(shù)是在singleflight
的互斥鎖保護(hù)下進(jìn)行的,在WaitGroup
完成之前可以讀寫,在WaitGroup
完成后只能讀取。目前該字段的作用是判斷call
的結(jié)果是否被共享。chans []chan<- Result
:一個通道切片,用于存儲所有等待當(dāng)前call
結(jié)果的通道。這些通道在call
完成時接收到結(jié)果。這個字段同樣是在singleflight
的互斥鎖保護(hù)下進(jìn)行的,在WaitGroup
完成之前可以讀寫,在WaitGroup
完成后只能讀取。
一句話概括就是:call
結(jié)構(gòu)體用于跟蹤 Do
或 DoChan
方法的調(diào)用狀態(tài),包括等待其完成的 goroutine
、調(diào)用的結(jié)果、發(fā)生的錯誤以及跟蹤重復(fù)的調(diào)用次數(shù),對于 singleflight
在共享調(diào)用結(jié)果中起到關(guān)鍵作用。
Result
Result
是一個封裝了請求調(diào)用結(jié)果的結(jié)構(gòu)體,在DoChan
方法返回結(jié)果時使用。該結(jié)構(gòu)體的源碼如下所示:
// Result holds the results of Do, so they can be passed // on a channel. type Result struct { Val interface{} Err error Shared bool }
Result
結(jié)構(gòu)體有三個字段:
Val interface{}
:存儲DoChan
方法調(diào)用的結(jié)果值之一,對應(yīng)著fn
函數(shù)(DoChan
方法的參數(shù))的第一個返回值val
。Err error
:存儲DoChan
方法調(diào)用過程中可能發(fā)生的錯誤。Shared bool
:表示調(diào)用結(jié)果是否被多個請求(goroutine
)共享。該字段的值,取決于call
結(jié)構(gòu)體的dups
字段值,如果dups
大于 0,Shared
的值則為true
,否則為false
。
panicError
panicError
用于封裝從 panic
中恢復(fù)的任意值和在給定函數(shù)執(zhí)行期間產(chǎn)生的堆棧跟蹤信息。該結(jié)構(gòu)體的源碼如下所示:
// A panicError is an arbitrary value recovered from a panic // with the stack trace during the execution of given function. type panicError struct { value interface{} stack []byte } // Error implements error interface. func (p *panicError) Error() string { return fmt.Sprintf("%v\n\n%s", p.value, p.stack) } func (p *panicError) Unwrap() error { err, ok := p.value.(error) if !ok { return nil } return err } func newPanicError(v interface{}) error { stack := debug.Stack() // The first line of the stack trace is of the form "goroutine N [status]:" // but by the time the panic reaches Do the goroutine may no longer exist // and its status will have changed. Trim out the misleading line. if line := bytes.IndexByte(stack[:], '\n'); line >= 0 { stack = stack[line+1:] } return &panicError{value: v, stack: stack} }
字段
panicError
結(jié)構(gòu)體有兩個字段:
value interface{}
:存儲從panic
中恢復(fù)的值,這個值是任意類型的,可能是error
類型,也可能是其它類型。stack []byte
:存儲堆棧跟蹤信息的字節(jié)切片,這個堆棧跟蹤提供了panic
發(fā)生時的函數(shù)調(diào)用層次結(jié)構(gòu)和順序,有助于調(diào)試和診斷問題。
方法
panicError
結(jié)構(gòu)體有兩個方法:
Error() string
:實現(xiàn)了error
接口的Error
方法。它將panicError
結(jié)構(gòu)體的value
和stack
字段的格式化成一個字符串。Unwrap() error
:實現(xiàn)了Wrapper
接口的Unwrap
接包方法,嘗試將value
字段斷言為error
類型并返回。如果value
不是一個error
類型,它將返回nil
。這個方法使得panicError
能夠與 Go 的錯誤處理機(jī)制(如errors.Is
和errors.As
)更好地集成。
初始化函數(shù)
newPanicError(v interface{}) error
:這個函數(shù)用于創(chuàng)建一個新的 panicError
實例。它接受從 panic
中恢復(fù)的值作為參數(shù),然后通過 debug.Stack
獲取堆棧信息,并移除堆棧信息的第一行(如 goroutine
的編號和狀態(tài)),因為這一行包含的信息可能會因為 panic
的恢復(fù)而變得不準(zhǔn)確。最后,返回指向 panicError
實例的指針。
核心方法解析
Do
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) { // 獲取鎖 g.mu.Lock() // 懶初始化 map if g.m == nil { g.m = make(map[string]*call) } // 判斷特定 key 的 call 是否正在進(jìn)行調(diào)用 if c, ok := g.m[key]; ok { // 重復(fù)調(diào)用次數(shù)加 1 c.dups++ // 解鎖 g.mu.Unlock() // 掛起,等待調(diào)用的完成 c.wg.Wait() // 判斷是否發(fā)生了 panic if e, ok := c.err.(*panicError); ok { // panic panic(e) } else if c.err == errGoexit { // 判斷是否發(fā)生了 runtime.Goexit // 執(zhí)行 runtime.Goexit,停止當(dāng)前 goroutine 的執(zhí)行,并確保所有 defer 語句的執(zhí)行 runtime.Goexit() } // 返回結(jié)果 return c.val, c.err, true } // 創(chuàng)建一個新的調(diào)用 c := new(call) // 等待組加 1 c.wg.Add(1) // key 和 call 映射 g.m[key] = c // 釋放鎖 g.mu.Unlock() // 調(diào)用開始,執(zhí)行所接受的函數(shù) fn g.doCall(c, key, fn) // 返回結(jié)果 return c.val, c.err, c.dups > 0 }
Do
方法的執(zhí)行流程如下所示:
1、獲取鎖:通過 g.mu.Lock()
加鎖,確保對內(nèi)部的 g.m
(一個 map
,用于跟蹤 key
的調(diào)用狀態(tài)) 和 c.dups
(對于該 key
的重復(fù)調(diào)用次數(shù)) 的訪問是并發(fā)安全的。
2、初始化 map
:如果 g.m == nil
,意味著是第一次調(diào)用 Do
方法且沒有調(diào)用過 DoChan
方法,所以初始化 g.m
。
3、檢查是否有正在進(jìn)行的調(diào)用:通過 c, ok := g.m[key]; ok
檢查是否有一個對于該 key
的調(diào)用正在進(jìn)行,如果 ok
為 true
,則說明有一個對于該 key
的調(diào)用正在進(jìn)行:
- 增加重復(fù)調(diào)用次數(shù)
c.dups
,表示來了一個新的goroutine
在等待這個調(diào)用結(jié)果。 - 釋放鎖
g.mu.Unlock()
,因為不再需要修改共享資源。 - 等待
c.wg.Wait()
,直到當(dāng)前的調(diào)用完成。 - 檢查錯誤類型,并按需處理(如果是
panicError
或errGoexit
,則分別觸發(fā)panic
或Goexit
)。 - 返回當(dāng)前進(jìn)行的調(diào)用的結(jié)果。
4、初始化并執(zhí)行新的調(diào)用:如果沒有一個對于該 key
的調(diào)用正在進(jìn)行,則:
- 創(chuàng)建一個新的
call
實例。 c.wg
等待組計數(shù)加 1,標(biāo)記新操作的開始,后續(xù)有相同調(diào)用的請求將會等待該操作的完成并共享結(jié)果。- 在
g.m
中注冊key
和新創(chuàng)建的call
實例的映射g.m[key] = c
。 - 釋放鎖。
- 調(diào)用
g.doCall(c, key, fn)
執(zhí)行實際的函數(shù)調(diào)用。 - 返回調(diào)用結(jié)果。
Do
方法的關(guān)鍵在于綜合使用等待組(sync.WaitGroup
)、互斥鎖(sync.Mutex
)以及一個映射(map
),以確保:
- 對于相同的
key
,fn
函數(shù)只會被執(zhí)行一次。這是通過map
檢查當(dāng)前key
是否存在對應(yīng)的call
實例來實現(xiàn)的。如果已存在,意味著函數(shù)調(diào)用正在執(zhí)行或已完成,不需要再次執(zhí)行。 - 同一時刻,所有請求同一
key
的調(diào)用都能得到同一個結(jié)果。通過map
追蹤特定key
對應(yīng)的調(diào)用結(jié)果,確保所有的goroutine
對同一key
發(fā)起Do
方法調(diào)用都能共享相同的結(jié)果。 - 正確地處理并發(fā)和同步。通過
sync.Mutex
保護(hù)并發(fā)環(huán)境下map
的讀寫操作,避免并發(fā)沖突;通過sync.WaitGroup
等待異步操作完成,保證所有請求都在函數(shù)執(zhí)行完成后才返回結(jié)果。
doCall
doCall
方法負(fù)責(zé)執(zhí)行給定 key
的函數(shù) fn
,并處理可能的錯誤和同步執(zhí)行結(jié)果,確保所有請求該key
的goroutine
得到統(tǒng)一的結(jié)果。該方法的源碼如下所示:
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) { // 定義正常返回標(biāo)志 normalReturn := false // 定義 panic 標(biāo)志 recovered := false // 使用雙重 defer 來區(qū)分 panic 和 runtime.Goexit defer func() { // fn 函數(shù)里面調(diào)用了 runtime.Goexit 函數(shù) if !normalReturn && !recovered { // 將 errGoexit 的值賦給 c.err c.err = errGoexit } // 加鎖 g.mu.Lock() // 函數(shù)執(zhí)行結(jié)束時釋放鎖 defer g.mu.Unlock() // 標(biāo)記 call 的完成 c.wg.Done() // 保險起見,判斷當(dāng)前 key 對應(yīng)的 call 是否被覆蓋,沒有被覆蓋就從 map 中移除這個 key if g.m[key] == c { delete(g.m, key) } // 判斷執(zhí)行 fn 的時候是否發(fā)生 panic if e, ok := c.err.(*panicError); ok { // 避免等待中的通道永久阻塞,如果發(fā)生了 panic,需要確保這個 panic 不能被捕獲 if len(c.chans) > 0 { // 開一個新的協(xié)程去 panic,這個 panic 就不會被捕獲了 go panic(e) // 保持當(dāng)前 goroutine 的存活,這樣等到 panic 之后,關(guān)于當(dāng)前 goroutine 的信息就會出現(xiàn)在堆棧中 select {} } else { // 直接 panic panic(e) } } else if c.err == errGoexit { // 如果是 errGoexit,什么都不用做,因為之前已經(jīng)執(zhí)行了 runtime.Goexit } else { // 向等待中的通道發(fā)送結(jié)果 for _, ch := range c.chans { ch <- Result{c.val, c.err, c.dups > 0} } } }() func() { defer func() { // 如果 fn 沒有正常執(zhí)行完 if !normalReturn { // 獲取從 panic 中恢復(fù)的值 if r := recover(); r != nil { // 創(chuàng)建一個 `panicError` 實例并賦值給 c.err c.err = newPanicError(r) } } }() // 執(zhí)行函數(shù)調(diào)用 c.val, c.err = fn() // 設(shè)置正常返回標(biāo)志為 true normalReturn = true }() // 如果 fn 沒有正常執(zhí)行完,則發(fā)生了 panic if !normalReturn { // 設(shè)置 panic 標(biāo)志為 true recovered = true } }
代碼剖析:
- 標(biāo)志位定義:定義
normalReturn
和recovered
用來區(qū)分fn
是否正常執(zhí)行完成或者發(fā)生了panic
。 - 雙重
defer
機(jī)制:目的是為了能夠區(qū)分fn
函數(shù)的正常執(zhí)行完成、fn
函數(shù)里發(fā)生的panic
以及fn
函數(shù)里調(diào)用runtime.Goexit
終止協(xié)程的情況。第一個
defer
用于清理資源和處理結(jié)果。- 如果非正常函數(shù)執(zhí)行完成并且沒有發(fā)生
panic
,則fn
里執(zhí)行了runtime.Goexit
函數(shù)。 - 加鎖,調(diào)用
c.wg.Done()
以標(biāo)記call
調(diào)用完成,然后從g.m
映射中移除當(dāng)前key
。 - 錯誤處理。
- 如果
fn
函數(shù)中發(fā)生了panic
,先判斷是否有通道正在等待結(jié)果,有的話,新開一個協(xié)程去panic
,確保panic
不能被恢復(fù),這里還用到了select{}
來阻塞當(dāng)前線程,保證panic
之后,當(dāng)前goroutine
的信息會出現(xiàn)在堆棧中。如果沒有通道正在等待結(jié)果,則直接panic
。 - 如果是
errGoexit
錯誤,說明fn
函數(shù)中執(zhí)行了runtime.Goexit
,這時什么都不用做。
- 如果
- 結(jié)果同步。如果沒有發(fā)生
error
,就向正在等待的通道發(fā)送結(jié)果。
- 如果非正常函數(shù)執(zhí)行完成并且沒有發(fā)生
第二個
defer
在一個匿名函數(shù)里,它的目的是執(zhí)行fn
函數(shù)和捕獲panic
。如果fn
函數(shù)正常執(zhí)行完成,normalReturn
就會被設(shè)置為true
;在defer
里,如果normalReturn
為false
,則說明可能發(fā)生了panic
,通過recover()
函數(shù)嘗試恢復(fù)panic
并新建一個panicError
存儲信息。
recovered
標(biāo)志更新:如果fn
函數(shù)非正常執(zhí)行成功(normalReturn
為false
),則將recovered
賦值為true
,表示發(fā)生了panic
。
call
方法的關(guān)鍵在于使用了雙重 defer
機(jī)制,結(jié)合標(biāo)志 normalReturn
和 recovered
來判斷 fn
函數(shù)的狀態(tài)。normalReturn
和 recovered
有三組值:
normalReturn
為true
,recovered
為false
:表明fn
函數(shù)執(zhí)行成功,后續(xù)執(zhí)行第一個defer
時,除了資源清理以外,還會向等待中的通道發(fā)送調(diào)用完成的結(jié)果。normalReturn
為false
,recovered
為true
:表明在fn
函數(shù)里發(fā)生了panic
,并且這個panic
被成功捕獲并恢復(fù)。后續(xù)執(zhí)行第一個defer
時,除了資源清理以外,會再次觸發(fā)panic
。normalReturn
為false
,recovered
為false
:這種情況說明在fn
函數(shù)里,調(diào)用了runtime.Goexit
函數(shù)終止當(dāng)前協(xié)程,不再執(zhí)行后續(xù)的代碼。這意味著normalReturn = true
和recovered = true
代碼都不可能被執(zhí)行,因此normalReturn
和recovered
的值都為false
。后續(xù)執(zhí)行第一個defer
時不會向等待的通道發(fā)送任何結(jié)果,僅僅是進(jìn)行資源清理。
DoChan
DoChan
方法與 Do
方法類似,但是它返回的是一個通道,通道在操作完成時接收到結(jié)果。返回值是通道,意味著我們能以非阻塞的方式等待結(jié)果。該方法的源碼如下所示:
func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result { // 創(chuàng)建一個通道,類型為 Result ch := make(chan Result, 1) // 加鎖 g.mu.Lock() // 懶初始化 map if g.m == nil { g.m = make(map[string]*call) } // 判定該 key 是否有正在進(jìn)行的調(diào)用 if c, ok := g.m[key]; ok { // 重復(fù)調(diào)用次數(shù)加 1 c.dups++ // 將新通道添加到通道切片里 c.chans = append(c.chans, ch) // 釋放鎖 g.mu.Unlock() // 返回通道 return ch } // 創(chuàng)建一個 call 實例,并將 ch 通道作為參數(shù)傳遞 c := &call{chans: []chan<- Result{ch}} // 等待組加 1 c.wg.Add(1) // key 和 call 映射 g.m[key] = c // 釋放鎖 g.mu.Unlock() // 異步執(zhí)行調(diào)用 go g.doCall(c, key, fn) // 返回通道 return ch }
DoChan
方法的執(zhí)行流程如下所示:
1、創(chuàng)建一個大小為 1 的緩沖通道。
2、獲取鎖:通過 g.mu.Lock()
加鎖,確保對內(nèi)部的 g.m
(一個 map
,用于跟蹤 key
的調(diào)用狀態(tài)) 和 c.dups
(對于該 key
的重復(fù)調(diào)用次數(shù))以及 c.chans
(通道切片) 的訪問是并發(fā)安全的。
3、初始化 map
:如果 g.m == nil
,意味著是第一次調(diào)用 Do
方法且沒有調(diào)用過 DoChan
方法,所以初始化 g.m
。
4、檢查是否有正在進(jìn)行的調(diào)用:通過 c, ok := g.m[key]; ok
檢查是否有一個對于該 key
的調(diào)用正在進(jìn)行,如果 ok
為 true
,則說明有一個對于該 key
的調(diào)用正在進(jìn)行:
- 增加重復(fù)調(diào)用次數(shù)
c.dups
,表示來了一個新的goroutine
在等待這個調(diào)用結(jié)果。 - 將新創(chuàng)建的通道追加到當(dāng)前
call
的通道切片里。 - 釋放鎖
g.mu.Unlock()
,因為不再需要修改共享資源。 - 返回新創(chuàng)建的通道。
5、初始化并異步執(zhí)行新的調(diào)用:如果沒有一個對于該 key
的調(diào)用正在進(jìn)行,則:
- 創(chuàng)建一個新的
call
實例,并關(guān)聯(lián)新創(chuàng)建的通道。 c.wg
等待組計數(shù)加 1,標(biāo)記新操作的開始,后續(xù)有相同調(diào)用的請求將會等待該操作的完成并共享結(jié)果。- 在
g.m
中注冊key
和新創(chuàng)建的call
實例的映射g.m[key] = c
。 - 釋放鎖。
- 異步調(diào)用
g.doCall(c, key, fn)
執(zhí)行實際的函數(shù)調(diào)用。 - 返回新創(chuàng)建的通道。
DoChan
與 Do
方法的區(qū)別在于同步共享結(jié)果的方式:
Do
方法:
- 如果有其他請求正在進(jìn)行(對同一個
key
),它會使用sync.WaitGroup
等待這個請求完成以共享結(jié)果。 - 如果是針對給定
key
的新請求,它將直接啟動doCall
來執(zhí)行函數(shù)調(diào)用,等待執(zhí)行完成且call
實例的更新,然后返回結(jié)果。
DoChan
方法:為每個調(diào)用創(chuàng)建一個新的通道,將其加入到對應(yīng) key
的 call
實例的通道切片里,然后返回一個通道。這樣,等 g.doCall
正常異步調(diào)用完成后,會向各個通道發(fā)送結(jié)果。
Forget
Forget
方法用于從 g.m
移除特定 key
的調(diào)用。
func (g *Group) Forget(key string) { // 加鎖 g.mu.Lock() // 移除特定的 key delete(g.m, key) // 釋放鎖 g.mu.Unlock() }
該方法在刪除特定 key
前執(zhí)行加鎖操作,保護(hù)并發(fā)環(huán)境下 map
的讀寫操作,避免并發(fā)沖突。
小結(jié)
本文對 Go
singleflight
的源碼進(jìn)行剖析,該包的主要作用是用于防止重復(fù)的請求,它確保給定的 key
,函數(shù)在同一時間內(nèi)只執(zhí)行一次,多個請求共享同一結(jié)果。singleflight
能實現(xiàn)這種效果,關(guān)鍵點在于:
將多個相同請求合并成一個請求,確保函數(shù)只執(zhí)行一次:singleflight
為了解決這個問題,引入了互斥鎖 sync.Mutex
和 map
。
互斥鎖用于保護(hù)在并發(fā)環(huán)境下 map
的讀寫操作,避免并發(fā)沖突。
map
則負(fù)責(zé)將每一個唯一的 key
映射到 call
實例上,該實例包含了fn
函數(shù)的返回值和可能的錯誤等。
- 遇到一個尚未在
map
中記錄的key
請求時,創(chuàng)建并執(zhí)行一個新的call
實例。 - 如果
map
中已存在該key
對應(yīng)的call
實例,表明有一個相同的請求正在執(zhí)行或已完成,此時僅需等待此call
完成并直接其共享結(jié)果。
結(jié)果共享機(jī)制:singleflight
通過阻塞式和非阻塞式兩種方式,實現(xiàn)了結(jié)果的共享。
阻塞式機(jī)制:當(dāng)多個請求通過 Do
方法進(jìn)行相同的調(diào)用時,它們處于等待狀態(tài)(里面借助了 sync.WaitGroup
來實現(xiàn)阻塞的效果),直到首個請求的 fn
函數(shù)的執(zhí)行完畢。此后,等待的請求會接收到已完成的請求結(jié)果。
非阻塞式機(jī)制:相比于阻塞等待,當(dāng)請求通過 DoChan
方法發(fā)起時,每個請求會立即獲得一個專屬的通道。這些請求可以繼續(xù)執(zhí)行其他操作,直到它們準(zhǔn)備好從各自的通道接收結(jié)果。在接收結(jié)果時,如果結(jié)果尚未發(fā)送過來,也會暫時處于阻塞狀態(tài)。
除了以上兩個關(guān)鍵點,還需要考慮錯誤的處理,singleflight
通過使用雙重 defer
的機(jī)制,用于辨別 函數(shù)正常執(zhí)行完成、函數(shù)里發(fā)生了 panic
以及 函數(shù)里調(diào)用了 runtime.Goexit()
函數(shù) 三種情況,每種情況采取不同的處理機(jī)制。
到此這篇關(guān)于源碼剖析Golang中singleflight的應(yīng)用的文章就介紹到這了,更多相關(guān)Go singleflight內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang標(biāo)準(zhǔn)庫SSH操作示例詳解
文章介紹了如何使用Golang的crypto/ssh庫實現(xiàn)SSH客戶端功能,包括連接遠(yuǎn)程服務(wù)器、執(zhí)行命令、捕獲輸出以及與os/exec標(biāo)準(zhǔn)庫的對比,本文給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2024-12-12關(guān)于golang中死鎖的思考與學(xué)習(xí)
本文主要介紹了關(guān)于golang中死鎖的思考與學(xué)習(xí),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03golang解析網(wǎng)頁利器goquery的使用方法
這篇文章主要給大家介紹了關(guān)于golang解析網(wǎng)頁利器goquery的使用方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考借鑒,下面來一起學(xué)習(xí)學(xué)習(xí)吧。2017-09-09goFrame的隊列g(shù)queue對比channel使用詳解
這篇文章主要為大家介紹了goFrame的gqueue對比channel使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06gin自定義中間件解決requestBody不可重讀(請求體取值)
這篇文章主要介紹了gin自定義中間件解決requestBody不可重讀,確??刂破髂軌颢@取請求體值,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10golang實現(xiàn)多協(xié)程下載文件(支持?jǐn)帱c續(xù)傳)
本文主要介紹了golang實現(xiàn)多協(xié)程下載文件,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-11-11golang NewRequest/gorequest實現(xiàn)http請求的示例代碼
本文主要介紹了golang NewRequest/gorequest實現(xiàn)http請求的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08go 下載非標(biāo)準(zhǔn)庫包(部份包被墻了)到本地使用的方法
今天小編就為大家分享一篇go 下載非標(biāo)準(zhǔn)庫包(部份包被墻了)到本地使用的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-06-06