一文帶你了解Golang中select的實現(xiàn)原理
概述
select是go提供的一種跟并發(fā)相關(guān)的語法,非常有用。本文將介紹 Go 語言中的 select
的實現(xiàn)原理,包括 select
的結(jié)構(gòu)和常見問題、編譯期間的多種優(yōu)化以及運行時的執(zhí)行過程。
select
是一種與 switch
非常相似的控制結(jié)構(gòu),與 switch
不同的是,select
中雖然也有多個 case
,但是這些 case
中的表達(dá)式都必須與 Channel 的操作有關(guān),也就是 Channel 的讀寫操作,下面的函數(shù)就展示了一個包含從 Channel 中讀取數(shù)據(jù)和向 Channel 發(fā)送數(shù)據(jù)的 select
結(jié)構(gòu):
func fibonacci(c, quit chan int) { x, y := 0, 1 for { select { case c <- x: x, y = y, x+y case <-quit: fmt.Println("quit") return } } }
這個 select
控制結(jié)構(gòu)就會等待 c <- x
或者 <-quit
兩個表達(dá)式中任意一個的返回,無論哪一個返回都會立刻執(zhí)行 case
中的代碼,不過如果了 select
中的兩個 case
同時被觸發(fā),就會隨機選擇一個 case
執(zhí)行。
結(jié)構(gòu)
select
在 Go 語言的源代碼中其實不存在任何的結(jié)構(gòu)體表示,但是 select
控制結(jié)構(gòu)中 case
卻使用了 scase
結(jié)構(gòu)體來表示:
type scase struct { c *hchan elem unsafe.Pointer kind uint16 pc uintptr releasetime int64 }
由于非 default
的 case
中都與 Channel 的發(fā)送和接收數(shù)據(jù)有關(guān),所以在 scase
結(jié)構(gòu)體中也包含一個 c
字段用于存儲 case
中使用的 Channel,elem
是用于接收或者發(fā)送數(shù)據(jù)的變量地址、kind
表示當(dāng)前 case
的種類,總共包含以下四種:
const ( caseNil = iota caseRecv caseSend caseDefault )
這四種常量分別表示不同類型的 case
,相信它們的命名已經(jīng)能夠充分幫助我們理解它們的作用了,所以在這里也不再展開介紹了。
現(xiàn)象
當(dāng)我們在 Go 語言中使用 select
控制結(jié)構(gòu)時,其實會遇到兩個非常有趣的問題,一個是 select
能在 Channel 上進(jìn)行非阻塞的收發(fā)操作,另一個是 select
在遇到多個 Channel 同時響應(yīng)時能夠隨機挑選 case
執(zhí)行。
非阻塞的收發(fā)
如果一個 select
控制結(jié)構(gòu)中包含一個 default
表達(dá)式,那么這個 select
并不會等待其它的 Channel 準(zhǔn)備就緒,而是會非阻塞地讀取或者寫入數(shù)據(jù):
func main() { ch := make(chan int) select { case i := <-ch: println(i) default: println("default") } } $ go run main.go default
當(dāng)我們運行上面的代碼時其實也并不會阻塞當(dāng)前的 Goroutine,而是會直接執(zhí)行 default
條件中的內(nèi)容并返回。
隨機執(zhí)行
另一個使用 select
遇到的情況其實就是同時有多個 case
就緒后,select
如何進(jìn)行選擇的問題,我們通過下面的代碼可以簡單了解一下:
func main() { ch := make(chan int) go func() { for range time.Tick(1 * time.Second) { ch <- 0 } }() for { select { case <-ch: println("case1") case <-ch: println("case2") } } } $ go run main.go case1 case2 case1 case2 case2 case1 ...
從上述代碼輸出的結(jié)果中我們可以看到,select
在遇到兩個 <-ch
同時響應(yīng)時其實會隨機選擇一個 case
執(zhí)行其中的表達(dá)式,我們會在這一節(jié)中介紹這一現(xiàn)象的實現(xiàn)原理。
編譯
select
語句在編譯期間會被轉(zhuǎn)換成 OSELECT
節(jié)點,每一個 OSELECT
節(jié)點都會持有一系列的 OCASE
節(jié)點,如果 OCASE
節(jié)點的都是空的,就意味著這是一個 default
節(jié)點:
上圖展示的其實就是 select 在編譯期間的結(jié)構(gòu),每一個 OCASE 既包含了執(zhí)行條件也包含了滿足條件后執(zhí)行的代碼,我們在這一節(jié)中就會介紹 select 語句在編譯期間進(jìn)行的優(yōu)化和轉(zhuǎn)換。
編譯器在中間代碼生成期間會根據(jù) select 中 case 的不同對控制語句進(jìn)行優(yōu)化,這一過程其實都發(fā)生在 walkselectcases 函數(shù)中,我們在這里會分四種情況分別介紹優(yōu)化的過程和結(jié)果:
select 中不存在任何的 case;
select 中只存在一個 case;
select 中存在兩個 case,其中一個 case 是 default 語句;
通用的 select 條件;
我們會按照這四種不同的情況拆分 walkselectcases 函數(shù)并分別介紹不同場景下優(yōu)化的結(jié)果。
直接阻塞
首先介紹的其實就是最簡單的情況,也就是當(dāng) select
結(jié)構(gòu)中不包含任何的 case
時,編譯器是如何進(jìn)行處理的:
func walkselectcases(cases *Nodes) []*Node { n := cases.Len() if n == 0 { return []*Node{mkcall("block", nil, nil)} } // ... }
這段代碼非常簡單并且容易理解,它直接將類似 select {}
的空語句,轉(zhuǎn)換成對 block
函數(shù)的調(diào)用:
func block() { gopark(nil, nil, waitReasonSelectNoCases, traceEvGoStop, 1) }
block
函數(shù)的實現(xiàn)非常簡單,它會運行 gopark
讓出當(dāng)前 Goroutine 對處理器的使用權(quán),該 Goroutine 也會進(jìn)入永久休眠的狀態(tài)也沒有辦法被其他的 Goroutine 喚醒,我們可以看到調(diào)用 gopark
方法時傳入的等待原因是 waitReasonSelectNoCases
,這其實也在告訴我們一個空的 select
語句會直接阻塞當(dāng)前的 Goroutine。
獨立情況
如果當(dāng)前的 select
條件只包含一個 case
,那么就會就會執(zhí)行如下的優(yōu)化策略將原來的 select
語句改寫成 if
條件語句,下面是在 select
中從 Channel 接受數(shù)據(jù)時被改寫的情況:
select { case v, ok <-ch: // ... } if ch == nil { block() } v, ok := <-ch // ...
在 walkselectcases
函數(shù)中,如果只包含一個發(fā)送的 case
,那么就不會包含 v, ok := <- ch
這個表達(dá)式,因為向 Channel 發(fā)送數(shù)據(jù)并沒有任何的返回值。
我們可以看到如果在 select
中僅存在一個 case
,那么當(dāng) case
中處理的 Channel 是空指針時,就會發(fā)生和沒有 case
的 select
語句一樣的情況,也就是直接掛起當(dāng)前 Goroutine 并且永遠(yuǎn)不會被喚醒。
非阻塞操作
在下一次的優(yōu)化策略執(zhí)行之前,walkselectcases
函數(shù)會先將 case
中所有 Channel 都轉(zhuǎn)換成指向 Channel 的地址以便于接下來的優(yōu)化和通用邏輯的執(zhí)行,改寫之后就會進(jìn)行最后一次的代碼優(yōu)化,觸發(fā)的條件就是 — select
中包含兩個 case
,但是其中一個是 default
,我們可以分成發(fā)送和接收兩種情況介紹處理的過程。
發(fā)送
首先就是 Channel 的發(fā)送過程,也就是 case
中的表達(dá)式是 OSEND
類型,在這種情況下會使用 if/else
語句改寫代碼:
select { case ch <- i: // ... default: // ... } if selectnbsend(ch, i) { // ... } else { // ... }
這里最重要的函數(shù)其實就是 selectnbsend
,它的主要作用就是非阻塞地向 Channel 中發(fā)送數(shù)據(jù),我們在 Channel 一節(jié)曾經(jīng)提到過發(fā)送數(shù)據(jù)的 chansend
函數(shù)包含一個 block
參數(shù),這個參數(shù)會決定這一次的發(fā)送是不是阻塞的:
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) { return chansend(c, elem, false, getcallerpc()) }
在這里我們只需要知道當(dāng)前的發(fā)送過程不是阻塞的,哪怕是沒有接收方、緩沖區(qū)空間不足導(dǎo)致失敗了也會立即返回。
接收
由于從 Channel 中接收數(shù)據(jù)可能會返回一個或者兩個值,所以這里的情況會比發(fā)送時稍顯復(fù)雜,不過改寫的套路和邏輯確是差不多的:
select { case v <- ch: // case v, received <- ch: // ... default: // ... } if selectnbrecv(&v, ch) { // if selectnbrecv2(&v, &received, ch) { // ... } else { // ... }
返回值數(shù)量不同會導(dǎo)致最終使用函數(shù)的不同,兩個用于非阻塞接收消息的函數(shù) selectnbrecv
和 selectnbrecv2
其實只是對 chanrecv
返回值的處理稍有不同:
func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected bool) { selected, _ = chanrecv(c, elem, false) return } func selectnbrecv2(elem unsafe.Pointer, received *bool, c *hchan) (selected bool) { selected, *received = chanrecv(c, elem, false) return }
因為接收方不需要,所以 selectnbrecv
會直接忽略返回的布爾值,而 selectnbrecv2
會將布爾值回傳給上層;與 chansend
一樣,chanrecv
也提供了一個 block
參數(shù)用于控制這一次接收是否阻塞。
通用情況
在默認(rèn)的情況下,select
語句會在編譯階段經(jīng)過如下過程的處理:
- 將所有的
case
轉(zhuǎn)換成包含 Channel 以及類型等信息的scase
結(jié)構(gòu)體; - 調(diào)用運行時函數(shù)
selectgo
獲取被選擇的scase
結(jié)構(gòu)體索引,如果當(dāng)前的scase
是一個接收數(shù)據(jù)的操作,還會返回一個指示當(dāng)前case
是否是接收的布爾值; - 通過
for
循環(huán)生成一組if
語句,在語句中判斷自己是不是被選中的case
一個包含三個 case
的正常 select
語句其實會被展開成如下所示的邏輯,我們可以看到其中處理的三個部分:
selv := [3]scase{} order := [6]uint16 for i, cas := range cases { c := scase{} c.kind = ... c.elem = ... c.c = ... } chosen, revcOK := selectgo(selv, order, 3) if chosen == 0 { // ... break } if chosen == 1 { // ... break } if chosen == 2 { // ... break }
展開后的 select
其實包含三部分,最開始初始化數(shù)組并轉(zhuǎn)換 scase
結(jié)構(gòu)體,使用 selectgo
選擇執(zhí)行的 case
以及最后通過 if
判斷選中的情況并執(zhí)行 case
中的表達(dá)式,需要注意的是這里其實也僅僅展開了 select
控制結(jié)構(gòu),select
語句執(zhí)行最重要的過程其實也是選擇 case
執(zhí)行的過程,這是我們在下一節(jié)運行時重點介紹的。
運行時
我們已經(jīng)充分地了解了 select 在編譯期間的處理過程,接下來可以展開介紹 selectgo 函數(shù)的實現(xiàn)原理了。
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) { } selectgo 是會在運行期間運行的函數(shù),這個函數(shù)的主要作用就是從 select 控制結(jié)構(gòu)中的多個 case 中選擇一個需要執(zhí)行的 case,隨后的多個 if 條件語句就會根據(jù) selectgo 的返回值執(zhí)行相應(yīng)的語句。
初始化
selectgo
函數(shù)首先會進(jìn)行執(zhí)行必要的一些初始化操作,也就是決定處理 case
的兩個順序,其中一個是 pollOrder
另一個是 lockOrder
:
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) { cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0)) order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0)) scases := cas1[:ncases:ncases] pollorder := order1[:ncases:ncases] lockorder := order1[ncases:][:ncases:ncases] for i := range scases { cas := &scases[i] if cas.c == nil && cas.kind != caseDefault { *cas = scase{} } } for i := 1; i < ncases; i++ { j := fastrandn(uint32(i + 1)) pollorder[i] = pollorder[j] pollorder[j] = uint16(i) } // sort the cases by Hchan address to get the locking order. // ... sellock(scases, lockorder) // ... }
Channel 的輪詢順序是通過 fastrandn
隨機生成的,這其實就導(dǎo)致了如果多個 Channel 同時『響應(yīng)』,select
會隨機選擇其中的一個執(zhí)行;而另一個 lockOrder
就是根據(jù) Channel 的地址確定的,根據(jù)相同的順序鎖定 Channel 能夠避免死鎖的發(fā)生,最后調(diào)用的 sellock
就會按照之前生成的順序鎖定所有的 Channel。
循環(huán)
當(dāng)我們?yōu)?nbsp;select
語句確定了輪詢和鎖定的順序并鎖定了所有的 Channel 之后就會開始進(jìn)入 select
的主循環(huán),查找或者等待 Channel 準(zhǔn)備就緒,循環(huán)中會遍歷所有的 case
并找到需要被喚起的 sudog
結(jié)構(gòu)體,在這段循環(huán)的代碼中,我們會分四種不同的情況處理 select
中的多個 case
:
caseNil
— 當(dāng)前 case
不包含任何的 Channel,就直接會被跳過;
caseRecv
— 當(dāng)前 case
會從 Channel 中接收數(shù)據(jù);
- 如果當(dāng)前 Channel 的
sendq
上有等待的 Goroutine 就會直接跳到recv
標(biāo)簽所在的代碼段,從 Goroutine 中獲取最新發(fā)送的數(shù)據(jù); - 如果當(dāng)前 Channel 的緩沖區(qū)不為空就會跳到
bufrecv
標(biāo)簽處從緩沖區(qū)中獲取數(shù)據(jù); - 如果當(dāng)前 Channel 已經(jīng)被關(guān)閉就會跳到
rclose
做一些清除的收尾工作;
caseSend
— 當(dāng)前 case
會向 Channel 發(fā)送數(shù)據(jù);
- 如果當(dāng)前 Channel 已經(jīng)被關(guān)閉就會直接跳到
rclose
代碼段; - 如果當(dāng)前 Channel 的
recvq
上有等待的 Goroutine 就會跳到send
代碼段向 Channel 直接發(fā)送數(shù)據(jù);
caseDefault
— 當(dāng)前 case
表示默認(rèn)情況,如果循環(huán)執(zhí)行到了這種情況就表示前面的所有 case
都沒有被執(zhí)行,所以這里會直接解鎖所有的 Channel 并退出 selectgo
函數(shù),這時也就意味著當(dāng)前 select
結(jié)構(gòu)中的其他收發(fā)語句都是非阻塞的。
這其實是循環(huán)執(zhí)行的第一次遍歷,主要作用就是尋找所有 case
中 Channel 是否有可以立刻被處理的情況,無論是在包含等待的 Goroutine 還是緩沖區(qū)中存在數(shù)據(jù),只要滿足條件就會立刻處理,如果不能立刻找到活躍的 Channel 就會進(jìn)入循環(huán)的下一個過程,按照需要將當(dāng)前的 Goroutine 加入到所有 Channel 的 sendq
或者 recvq
隊列中:
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) { // ... gp = getg() nextp = &gp.waiting for _, casei := range lockorder { casi = int(casei) cas = &scases[casi] if cas.kind == caseNil { continue } c = cas.c sg := acquireSudog() sg.g = gp sg.isSelect = true sg.elem = cas.elem sg.c = c *nextp = sg nextp = &sg.waitlink switch cas.kind { case caseRecv: c.recvq.enqueue(sg) case caseSend: c.sendq.enqueue(sg) } } gp.param = nil gopark(selparkcommit, nil, waitReasonSelect, traceEvGoBlockSelect, 1) // ... }
這里創(chuàng)建 sudog
并入隊的過程其實和 Channel 中直接進(jìn)行發(fā)送和接收時的過程幾乎完全相同,只是除了在入隊之外,這些 sudog
結(jié)構(gòu)體都會被串成鏈表附著在當(dāng)前 Goroutine 上,在入隊之后會調(diào)用 gopark
函數(shù)掛起當(dāng)前的 Goroutine 等待調(diào)度器的喚醒。
等到 select
對應(yīng)的一些 Channel 準(zhǔn)備好之后,當(dāng)前 Goroutine 就會被調(diào)度器喚醒,這時就會繼續(xù)執(zhí)行 selectgo
函數(shù)中剩下的邏輯,也就是從上面 入隊的 sudog
結(jié)構(gòu)體中獲取數(shù)據(jù):
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) { // ... gp.selectDone = 0 sg = (*sudog)(gp.param) gp.param = nil casi = -1 cas = nil sglist = gp.waiting gp.waiting = nil for _, casei := range lockorder { k = &scases[casei] if sg == sglist { casi = int(casei) cas = k } else { if k.kind == caseSend { c.sendq.dequeueSudoG(sglist) } else { c.recvq.dequeueSudoG(sglist) } } sgnext = sglist.waitlink sglist.waitlink = nil releaseSudog(sglist) sglist = sgnext } c = cas.c if cas.kind == caseRecv { recvOK = true } selunlock(scases, lockorder) goto retc // ... }
在第三次根據(jù) lockOrder
遍歷全部 case
的過程中,我們會先獲取 Goroutine 接收到的參數(shù) param
,這個參數(shù)其實就是被喚醒的 sudog
結(jié)構(gòu),我們會依次對比所有 case
對應(yīng)的 sudog
結(jié)構(gòu)找到被喚醒的 case
并釋放其他未被使用的 sudog
結(jié)構(gòu)。
由于當(dāng)前的 select
結(jié)構(gòu)已經(jīng)挑選了其中的一個 case
進(jìn)行執(zhí)行,那么剩下 case
中沒有被用到的 sudog
其實就會直接忽略并且釋放掉了,為了不影響 Channel 的正常使用,我們還是需要將這些廢棄的 sudog
從 Channel 中出隊;而除此之外的發(fā)生事件導(dǎo)致我們被喚醒的 sudog
結(jié)構(gòu)已經(jīng)在 Channel 進(jìn)行收發(fā)時就已經(jīng)出隊了,不需要我們再次處理,出隊的代碼以及相關(guān)分析其實都在 Channel 一節(jié)中發(fā)送和接收的章節(jié)。
當(dāng)我們在循環(huán)中發(fā)現(xiàn)緩沖區(qū)中有元素或者緩沖區(qū)未滿時就會通過 goto
關(guān)鍵字跳轉(zhuǎn)到以下的兩個代碼段,這兩段代碼的執(zhí)行過程其實都非常簡單,都只是向 Channel 中發(fā)送或者從緩沖區(qū)中直接獲取新的數(shù)據(jù):
bufrecv: recvOK = true qp = chanbuf(c, c.recvx) if cas.elem != nil { typedmemmove(c.elemtype, cas.elem, qp) } typedmemclr(c.elemtype, qp) c.recvx++ if c.recvx == c.dataqsiz { c.recvx = 0 } c.qcount-- selunlock(scases, lockorder) goto retc bufsend: typedmemmove(c.elemtype, chanbuf(c, c.sendx), cas.elem) c.sendx++ if c.sendx == c.dataqsiz { c.sendx = 0 } c.qcount++ selunlock(scases, lockorder) goto retc
這里在緩沖區(qū)中進(jìn)行的操作和直接對 Channel 調(diào)用 chansend
和 chanrecv
進(jìn)行收發(fā)的過程差不多,執(zhí)行結(jié)束之后就會直接跳到 retc
字段。
兩個直接收發(fā)的情況,其實也就是調(diào)用 Channel 運行時的兩個方法 send
和 recv
,這兩個方法會直接操作對應(yīng)的 Channel:
recv: recv(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2) recvOK = true goto retc send: send(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2) goto retc
不過當(dāng)發(fā)送或者接收時,情況就稍微有一點復(fù)雜了,從一個關(guān)閉 Channel 中接收數(shù)據(jù)會直接清除 Channel 中的相關(guān)內(nèi)容,而向一個關(guān)閉的 Channel 發(fā)送數(shù)據(jù)就會直接 panic
造成程序崩潰:
rclose: selunlock(scases, lockorder) recvOK = false if cas.elem != nil { typedmemclr(c.elemtype, cas.elem) } goto retc sclose: selunlock(scases, lockorder) panic(plainError("send on closed channel"))
總體來看,Channel 相關(guān)的收發(fā)操作和上一節(jié) Channel 實現(xiàn)原理中介紹的沒有太多出入,只是由于 select
多出了 default
關(guān)鍵字所以會出現(xiàn)非阻塞收發(fā)的情況。
總結(jié)
到這一節(jié)的最后我們需要總結(jié)一下,select
結(jié)構(gòu)的執(zhí)行過程與實現(xiàn)原理,首先在編譯期間,Go 語言會對 select
語句進(jìn)行優(yōu)化,以下是根據(jù) select
中語句的不同選擇了不同的優(yōu)化路徑:
空的 select
語句會被直接轉(zhuǎn)換成 block
函數(shù)的調(diào)用,直接掛起當(dāng)前 Goroutine;
如果 select
語句中只包含一個 case
,就會被轉(zhuǎn)換成 if ch == nil { block }; n;
表達(dá)式;
- 首先判斷操作的 Channel 是不是空的;
- 然后執(zhí)行
case
結(jié)構(gòu)中的內(nèi)容;
如果 select
語句中只包含兩個 case
并且其中一個是 default
,那么 Channel 和接收和發(fā)送操作都會使用 selectnbrecv
和 selectnbsend
非阻塞地執(zhí)行接收和發(fā)送操作;
在默認(rèn)情況下會通過 selectgo
函數(shù)選擇需要執(zhí)行的 case
并通過多個 if
語句執(zhí)行 case
中的表達(dá)式;
在編譯器已經(jīng)對 select
語句進(jìn)行優(yōu)化之后,Go 語言會在運行時執(zhí)行編譯期間展開的 selectgo
函數(shù),這個函數(shù)會按照以下的過程執(zhí)行:
1.隨機生成一個遍歷的輪詢順序 pollOrder
并根據(jù) Channel 地址生成一個用于遍歷的鎖定順序 lockOrder
;
2.根據(jù) pollOrder
遍歷所有的 case
查看是否有可以立刻處理的 Channel 消息;
- 如果有消息就直接獲取
case
對應(yīng)的索引并返回; - 如果沒有消息就會創(chuàng)建
sudog
結(jié)構(gòu)體,將當(dāng)前 Goroutine 加入到所有相關(guān) Channel 的sendq
和recvq
隊列中并調(diào)用gopark
觸發(fā)調(diào)度器的調(diào)度;
3.當(dāng)調(diào)度器喚醒當(dāng)前 Goroutine 時就會再次按照 lockOrder
遍歷所有的 case
,從中查找需要被處理的 sudog
結(jié)構(gòu)并返回對應(yīng)的索引;
然而并不是所有的 select
控制結(jié)構(gòu)都會走到 selectgo
上,很多情況都會被直接優(yōu)化掉,沒有機會調(diào)用 selectgo
函數(shù)。
Go 語言中的 select
關(guān)鍵字與 IO 多路復(fù)用中的 select
、epoll
等函數(shù)非常相似,不但 Channel 的收發(fā)操作與等待 IO 的讀寫能找到這種一一對應(yīng)的關(guān)系,這兩者的作用也非常相似;總的來說,select
關(guān)鍵字的實現(xiàn)原理稍顯復(fù)雜,與 Channel 的關(guān)系非常緊密,這里省略了很多 Channel 操作的細(xì)節(jié),數(shù)據(jù)結(jié)構(gòu)一章其實就介紹了 Channel 收發(fā)的相關(guān)細(xì)節(jié)。
到此這篇關(guān)于一文帶你了解Golang中select的實現(xiàn)原理的文章就介紹到這了,更多相關(guān)Golang select內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語言Elasticsearch數(shù)據(jù)清理工具思路詳解
這篇文章主要介紹了Go語言Elasticsearch數(shù)據(jù)清理工具思路詳解,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-10-10夯實Golang基礎(chǔ)之?dāng)?shù)據(jù)類型梳理匯總
這篇文章主要8為大家介紹了夯實Golang基礎(chǔ)之?dāng)?shù)據(jù)類型梳理匯總,有需要的朋友可以借鑒參考下,希望能夠有所幫助2023-10-10淺談beego默認(rèn)處理靜態(tài)文件性能低下的問題
下面小編就為大家?guī)硪黄獪\談beego默認(rèn)處理靜態(tài)文件性能低下的問題。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-06-06Go+Kafka實現(xiàn)延遲消息的實現(xiàn)示例
本文主要介紹了Go+Kafka實現(xiàn)延遲消息的實現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07Golang語言中的Prometheus的日志模塊使用案例代碼編寫
這篇文章主要介紹了Golang語言中的Prometheus的日志模塊使用案例,本文給大家分享源代碼編寫方法,感興趣的朋友跟隨小編一起看看吧2024-08-08go語言開發(fā)環(huán)境安裝及第一個go程序(推薦)
這篇文章主要介紹了go語言開發(fā)環(huán)境安裝及第一個go程序,這篇通過實例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2020-02-02Go使用Protocol?Buffers在數(shù)據(jù)序列化的優(yōu)勢示例詳解
這篇文章主要為大家介紹了Go使用Protocol?Buffers在數(shù)據(jù)序列化的優(yōu)勢示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11