源碼剖析Golang中singleflight的應(yīng)用
前言
前面的一篇文章 Go singleflight:防緩存擊穿利器 詳細介紹 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:一個互斥鎖,用于保護下面的m映射。在并發(fā)環(huán)境下,多個goroutine可能會同時對m進行讀寫操作,所以需要通過互斥鎖來確保對m的操作是安全的。m map[string]*call:一個map映射,其鍵是字符串,值是指向call結(jié)構(gòu)體的指針。Do和DoCHan方法的參數(shù)里,其中一個是key,m的鍵保存的就是這個key。m是惰性初始化的,意味著它在第一次使用時才會被創(chuàng)建。
Group 通過維護 m 字段來跟蹤每個 key 的調(diào)用狀態(tài),從而實現(xiàn)將多個請求合并成一個請求,多個請求共享同一個結(jié)果。
call
call 結(jié)構(gòu)體表示一個針對特定 key 的正在進行中或者已完成的請求,它確保所有同時對該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:一個等待組,用于等待當前call的完成。當調(diào)用Do或DoChan方法后,內(nèi)部會增加WaitGroup的計數(shù)器,當調(diào)用完成后,會減少計數(shù)器。在調(diào)用完成之前,其他想要獲取當前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ù)當前call的重復(fù)調(diào)用數(shù)量。這個計數(shù)是在singleflight的互斥鎖保護下進行的,在WaitGroup完成之前可以讀寫,在WaitGroup完成后只能讀取。目前該字段的作用是判斷call的結(jié)果是否被共享。chans []chan<- Result:一個通道切片,用于存儲所有等待當前call結(jié)果的通道。這些通道在call完成時接收到結(jié)果。這個字段同樣是在singleflight的互斥鎖保護下進行的,在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 的錯誤處理機制(如errors.Is和errors.As)更好地集成。
初始化函數(shù)
newPanicError(v interface{}) error:這個函數(shù)用于創(chuàng)建一個新的 panicError 實例。它接受從 panic 中恢復(fù)的值作為參數(shù),然后通過 debug.Stack 獲取堆棧信息,并移除堆棧信息的第一行(如 goroutine 的編號和狀態(tài)),因為這一行包含的信息可能會因為 panic 的恢復(fù)而變得不準確。最后,返回指向 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 是否正在進行調(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,停止當前 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、檢查是否有正在進行的調(diào)用:通過 c, ok := g.m[key]; ok 檢查是否有一個對于該 key 的調(diào)用正在進行,如果 ok 為 true,則說明有一個對于該 key 的調(diào)用正在進行:
- 增加重復(fù)調(diào)用次數(shù)
c.dups,表示來了一個新的goroutine在等待這個調(diào)用結(jié)果。 - 釋放鎖
g.mu.Unlock(),因為不再需要修改共享資源。 - 等待
c.wg.Wait(),直到當前的調(diào)用完成。 - 檢查錯誤類型,并按需處理(如果是
panicError或errGoexit,則分別觸發(fā)panic或Goexit)。 - 返回當前進行的調(diào)用的結(jié)果。
4、初始化并執(zhí)行新的調(diào)用:如果沒有一個對于該 key 的調(diào)用正在進行,則:
- 創(chuàng)建一個新的
call實例。 c.wg等待組計數(shù)加 1,標記新操作的開始,后續(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檢查當前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保護并發(fā)環(huán)境下map的讀寫操作,避免并發(fā)沖突;通過sync.WaitGroup等待異步操作完成,保證所有請求都在函數(shù)執(zhí)行完成后才返回結(jié)果。
doCall
doCall 方法負責執(zhí)行給定 key 的函數(shù) fn,并處理可能的錯誤和同步執(zhí)行結(jié)果,確保所有請求該key的goroutine 得到統(tǒng)一的結(jié)果。該方法的源碼如下所示:
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
// 定義正常返回標志
normalReturn := false
// 定義 panic 標志
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()
// 標記 call 的完成
c.wg.Done()
// 保險起見,判斷當前 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)
// 保持當前 goroutine 的存活,這樣等到 panic 之后,關(guān)于當前 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è)置正常返回標志為 true
normalReturn = true
}()
// 如果 fn 沒有正常執(zhí)行完,則發(fā)生了 panic
if !normalReturn {
// 設(shè)置 panic 標志為 true
recovered = true
}
}
代碼剖析:
- 標志位定義:定義
normalReturn和recovered用來區(qū)分fn是否正常執(zhí)行完成或者發(fā)生了panic。 - 雙重
defer機制:目的是為了能夠區(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()以標記call調(diào)用完成,然后從g.m映射中移除當前key。 - 錯誤處理。
- 如果
fn函數(shù)中發(fā)生了panic,先判斷是否有通道正在等待結(jié)果,有的話,新開一個協(xié)程去panic,確保panic不能被恢復(fù),這里還用到了select{}來阻塞當前線程,保證panic之后,當前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標志更新:如果fn函數(shù)非正常執(zhí)行成功(normalReturn為false),則將recovered賦值為true,表示發(fā)生了panic。
call 方法的關(guān)鍵在于使用了雙重 defer 機制,結(jié)合標志 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ù)終止當前協(xié)程,不再執(zhí)行后續(xù)的代碼。這意味著normalReturn = true和recovered = true代碼都不可能被執(zhí)行,因此normalReturn和recovered的值都為false。后續(xù)執(zhí)行第一個defer時不會向等待的通道發(fā)送任何結(jié)果,僅僅是進行資源清理。
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 是否有正在進行的調(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、檢查是否有正在進行的調(diào)用:通過 c, ok := g.m[key]; ok 檢查是否有一個對于該 key 的調(diào)用正在進行,如果 ok 為 true,則說明有一個對于該 key 的調(diào)用正在進行:
- 增加重復(fù)調(diào)用次數(shù)
c.dups,表示來了一個新的goroutine在等待這個調(diào)用結(jié)果。 - 將新創(chuàng)建的通道追加到當前
call的通道切片里。 - 釋放鎖
g.mu.Unlock(),因為不再需要修改共享資源。 - 返回新創(chuàng)建的通道。
5、初始化并異步執(zhí)行新的調(diào)用:如果沒有一個對于該 key 的調(diào)用正在進行,則:
- 創(chuàng)建一個新的
call實例,并關(guān)聯(lián)新創(chuàng)建的通道。 c.wg等待組計數(shù)加 1,標記新操作的開始,后續(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 方法:
- 如果有其他請求正在進行(對同一個
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í)行加鎖操作,保護并發(fā)環(huán)境下 map 的讀寫操作,避免并發(fā)沖突。
小結(jié)
本文對 Go singleflight 的源碼進行剖析,該包的主要作用是用于防止重復(fù)的請求,它確保給定的 key,函數(shù)在同一時間內(nèi)只執(zhí)行一次,多個請求共享同一結(jié)果。singleflight 能實現(xiàn)這種效果,關(guān)鍵點在于:
將多個相同請求合并成一個請求,確保函數(shù)只執(zhí)行一次:singleflight 為了解決這個問題,引入了互斥鎖 sync.Mutex 和 map。
互斥鎖用于保護在并發(fā)環(huán)境下 map 的讀寫操作,避免并發(fā)沖突。
map 則負責將每一個唯一的 key 映射到 call 實例上,該實例包含了fn 函數(shù)的返回值和可能的錯誤等。
- 遇到一個尚未在
map中記錄的key請求時,創(chuàng)建并執(zhí)行一個新的call實例。 - 如果
map中已存在該key對應(yīng)的call實例,表明有一個相同的請求正在執(zhí)行或已完成,此時僅需等待此call完成并直接其共享結(jié)果。
結(jié)果共享機制:singleflight 通過阻塞式和非阻塞式兩種方式,實現(xiàn)了結(jié)果的共享。
阻塞式機制:當多個請求通過 Do 方法進行相同的調(diào)用時,它們處于等待狀態(tài)(里面借助了 sync.WaitGroup 來實現(xiàn)阻塞的效果),直到首個請求的 fn 函數(shù)的執(zhí)行完畢。此后,等待的請求會接收到已完成的請求結(jié)果。
非阻塞式機制:相比于阻塞等待,當請求通過 DoChan 方法發(fā)起時,每個請求會立即獲得一個專屬的通道。這些請求可以繼續(xù)執(zhí)行其他操作,直到它們準備好從各自的通道接收結(jié)果。在接收結(jié)果時,如果結(jié)果尚未發(fā)送過來,也會暫時處于阻塞狀態(tài)。
除了以上兩個關(guān)鍵點,還需要考慮錯誤的處理,singleflight 通過使用雙重 defer 的機制,用于辨別 函數(shù)正常執(zhí)行完成、函數(shù)里發(fā)生了 panic 以及 函數(shù)里調(diào)用了 runtime.Goexit() 函數(shù) 三種情況,每種情況采取不同的處理機制。
到此這篇關(guān)于源碼剖析Golang中singleflight的應(yīng)用的文章就介紹到這了,更多相關(guān)Go singleflight內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang解析網(wǎng)頁利器goquery的使用方法
這篇文章主要給大家介紹了關(guān)于golang解析網(wǎng)頁利器goquery的使用方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考借鑒,下面來一起學習學習吧。2017-09-09
goFrame的隊列g(shù)queue對比channel使用詳解
這篇文章主要為大家介紹了goFrame的gqueue對比channel使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-06-06
gin自定義中間件解決requestBody不可重讀(請求體取值)
這篇文章主要介紹了gin自定義中間件解決requestBody不可重讀,確保控制器能夠獲取請求體值,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-10-10
golang實現(xiàn)多協(xié)程下載文件(支持斷點續(xù)傳)
本文主要介紹了golang實現(xiàn)多協(xié)程下載文件,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-11-11
golang NewRequest/gorequest實現(xiàn)http請求的示例代碼
本文主要介紹了golang NewRequest/gorequest實現(xiàn)http請求的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-08-08

