深入瞭解下 go 中的 select
前言
這裡藉助於幾個經常遇到的 select 的使用 demo 來作為開始,先來看看,下面幾個 demo 的輸出情況
1、栗子一
func main() {
chan1 := make(chan int)
chan2 := make(chan int)
go func() {
chan1 <- 1
}()
go func() {
chan2 <- 1
}()
select {
case <-chan1:
fmt.Println("chan1 ready.")
case <-chan2:
fmt.Println("chan2 ready.")
default:
fmt.Println("default")
}
}
select 中的 case 執行是隨機的,所以當 case 監聽的 channel 有資料傳入,就執行相應的流程並退出 select,如果對應的 case 沒有收到 channel 的資料,就執行 default 語句,然後退出 select。
上面的協程啟動時間是無法預估的,所以上面的兩個 case 和 default ,都有機會執行。
可能的輸出
可能輸出1、
chan1 ready.
可能輸出2、
chan2 ready.
可能輸出3、
default
2、栗子二
func main() {
chan1 := make(chan int)
chan2 := make(chan int)
go func() {
close(chan1)
}()
go func() {
close(chan2)
}()
select {
case <-chan1:
fmt.Println("chan1 ready.")
case <-chan2:
fmt.Println("chan2 ready.")
default:
fmt.Println("default")
}
}
已經關閉的 channel ,使用 select 是可以從中讀出對應的零值,同時兩面關閉 channel 的協程的執行實際也是不可控的,原則上,上面兩個 case 和 default 都有可能被執行。
可能的輸出
可能輸出1、
chan1 ready.
可能輸出2、
chan2 ready.
可能輸出3、
default
3、栗子三
func main() {
select {}
}
上面這個,應為沒有機會退出,所以會發生死鎖
看下原始碼實現
select 中的多個 case 是隨機觸發執行的,一次只有一個 case 得到執行。如果我們按照順序依次判斷,那麼後面的條件永遠都會得不到執行,而隨機的引入就是為了避免飢餓問題的發生。
1、如果沒有 default 分支
如果沒有 default 分支,select 將會一直處於阻塞狀態,直到其中的一個 case 就緒;
2、如果有 default 分支
如果有 default 分支,隨機將 case 分支遍歷一遍,如果有 case 分支可執行,處理對應的 case 分支;
如果遍歷完 case 分支,沒有可執行的分支,執行 default 分支。
原始碼版本 go version go1.16.13 darwin/amd64
原始碼包 src/runtime/select.go
定義了表示case語句的資料結構:
type scase struct {
c *hchan // chan
elem unsafe.Pointer // data element
}
c為當前 case 語句所操作的 channel 指標,這也說明了一個 case 語句只能操作一個 channel。
編譯階段,select 對應的 opType 是 OSELECT,select 語句在編譯期間會被轉換成 OSELECT 節點。
// https://github.com/golang/go/blob/release-branch.go1.16/src/cmd/compile/internal/gc/syntax.go#L922
OSELECT // select { List } (List is list of OCASE)
如果是 OSELECT 就會呼叫 walkselect()
,然後 walkselect()
最後呼叫 walkselectcases()
// https://github.com/golang/go/blob/release-branch.go1.16/src/cmd/compile/internal/gc/walk.go#L104
// The result of walkstmt MUST be assigned back to n, e.g.
// n.Left = walkstmt(n.Left)
func walkstmt(n *Node) *Node {
if n == nil {
return n
}
setlineno(n)
walkstmtlist(n.Ninit.Slice())
switch n.Op {
...
case OSELECT:
walkselect(n)
case OSWITCH:
walkswitch(n)
case ORANGE:
n = walkrange(n)
}
if n.Op == ONAME {
Fatalf("walkstmt ended up with name: %+v", n)
}
return n
}
// https://github.com/golang/go/blob/release-branch.go1.16/src/cmd/compile/internal/gc/select.go#L90
func walkselect(sel *Node) {
lno := setlineno(sel)
if sel.Nbody.Len() != 0 {
Fatalf("double walkselect")
}
init := sel.Ninit.Slice()
sel.Ninit.Set(nil)
// 呼叫walkselectcases
init = append(init, walkselectcases(&sel.List)...)
sel.List.Set(nil)
sel.Nbody.Set(init)
walkstmtlist(sel.Nbody.Slice())
lineno = lno
}
上面的呼叫邏輯,select 的邏輯是在 walkselectcases()
函式中完成的,這裡來重點看下
walkselectcases()
在處理中會分成下面幾種情況來處理
1、select 中不存在 case, 直接堵塞;
2、select 中僅存在一個 case;
3、select 中存在兩個 case,其中一個是 default;
4、其他 select 情況如: 包含多個 case 並且有 default 等。
// https://github.com/golang/go/blob/release-branch.go1.16/src/cmd/compile/internal/gc/select.go#L108
func walkselectcases(cases *Nodes) []*Node {
// 獲取 case 分支的數量
n := cases.Len()
// 優化: 沒有 case 的情況
if n == 0 {
// 翻譯為:block()
...
return
}
// 優化: 只有一個 case 的情況
if n == 1 {
// 翻譯為:if ch == nil { block() }; n;
...
return
}
// 優化: select 中存在兩個 case,其中一個是 default 的情況
if n == 2 {
// 翻譯為:傳送或接收
// if selectnbsend(c, v) { body } else { default body }
// 接收
// if selectnbrecv(&v, &received, c) { body } else { default body }
return
}
// 一般情況,呼叫 selecggo
...
}
1、不存在 case
如果不存在 case ,空的 select 語句會直接阻塞當前 Goroutine,導致 Goroutine 進入無法被喚醒的永久休眠狀態。
// https://github.com/golang/go/blob/release-branch.go1.16/src/cmd/compile/internal/gc/select.go#L108
func walkselectcases(cases *Nodes) []*Node {
n := cases.Len()
if n == 0 {
return []*Node{mkcall("block", nil, nil)}
}
...
}
// 呼叫 runtime.gopark 讓出當前 Goroutine 對處理器的使用權並傳入等待原因 waitReasonSelectNoCases。
func block() {
gopark(nil, nil, waitReasonSelectNoCases, traceEvGoStop, 1)
}
如果沒有 case,導致 Goroutine 進入無法被喚醒的永久休眠狀態,會觸發 deadlock!
2、select 中僅存在一個 case
如果只有一個 case ,編譯器會將 select 改寫成 if 條件語句。
// 改寫前
select {
case v, ok <-ch: // case ch <- v
...
}
// 改寫後
if ch == nil {
block()
}
v, ok := <-ch // case ch <- v
...
如果只有一個 case ,walkselectcases 會將 select 根據收發情況裝換成 if 語句,如果 case 中的 Channel 是空指標時,會直接掛起當前 Goroutine 並陷入永久休眠。
3、select 中存在兩個 case,其中一個是 default
傳送值
在 walkselectcases 中 OSEND,對應的就是向 channel 中傳送資料,如果是傳送的話,會翻譯成下面的語句
select {
case ch <- i:
...
default:
...
}
if selectnbsend(ch, i) {
...
} else {
// default body
...
}
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
return chansend(c, elem, false, getcallerpc())
}
如果是傳送,這裡翻譯之後最終呼叫 chansend 向 channel 中傳送資料
// 這裡提供了一個 block,引數設定成 true,那麼表示當前傳送操作是阻塞的
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
...
// 對於不阻塞的 send,快速檢測失敗場景
//
// 如果 channel 未關閉且 channel 沒有多餘的緩衝空間。這可能是:
// 1. channel 是非緩衝型的,且等待接收佇列裡沒有 goroutine
// 2. channel 是緩衝型的,但迴圈陣列已經裝滿了元素
if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||
(c.dataqsiz > 0 && c.qcount == c.dataqsiz)) {
return false
}
...
}
總結下
1、如果 block 為 true 表示當前向 channel 中的資料傳送是阻塞的。這裡可以看到 selectnbsend 中傳入的是 false,說明 channel 的傳送不會阻塞 select。
2、對於不阻塞的傳送,會進行下面的檢測,如果 channel 未關閉且 channel 沒有多餘的緩衝空間,就會傳送失敗,然後跳出當前的 case,走到 default 的邏輯。
如果 channel 未關閉且 channel 沒有多餘的緩衝空間。這可能是:
-
1、channel 是非緩衝型的,且等待接收佇列裡沒有 goroutine;
-
2、channel 是緩衝型的,但迴圈陣列已經裝滿了元素;
接收值
在 walkselectcases 函式中可以看到,接收方式會有兩個,分別是 OSELRECV 和 OSELRECV2
// https://github.com/golang/go/blob/release-branch.go1.16/src/cmd/compile/internal/gc/walk.go#L104
func walkselectcases(cases *Nodes) []*Node {
...
// optimization: two-case select but one is default: single non-blocking op.
if ncas == 2 && dflt != nil {
switch n.Op {
default:
Fatalf("select %v", n.Op)
case OSELRECV:
// if selectnbrecv(&v, c) { body } else { default body }
...
r.Left = mkcall1(chanfn("selectnbrecv", 2, ch.Type), types.Types[TBOOL], &r.Ninit, elem, ch)
case OSELRECV2:
// if selectnbrecv2(&v, &received, c) { body } else { default body }
...
r.Left = mkcall1(chanfn("selectnbrecv2", 2, ch.Type), types.Types[TBOOL], &r.Ninit, elem, receivedp, ch)
}
r.Left = typecheck(r.Left, ctxExpr)
r.Nbody.Set(cas.Nbody.Slice())
r.Rlist.Set(append(dflt.Ninit.Slice(), dflt.Nbody.Slice()...))
return []*Node{r, nod(OBREAK, nil, nil)}
}
...
}
walkselectcases 對這兩種情況的改寫
selectnbrecv
select {
case v = <-c:
...
default:
...
}
// 改寫後
if selectnbrecv(&v, c) {
...
} else {
// default body
...
}
selectnbrecv2
select {
case v, ok = <-c:
... foo
default:
... bar
}
// 改寫後
if c != nil && selectnbrecv2(&v, &ok, c) {
... foo
} else {
// default body
... bar
}
selectnbrecv 和 selectnbrecv2 有什麼區別呢?
// https://github.com/golang/go/blob/release-branch.go1.16/src/runtime/chan.go#L707
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) {
// TODO(khr): just return 2 values from this function, now that it is in Go.
selected, *received = chanrecv(c, elem, false)
return
}
可以發現只是針對返回的值處理不同,selectnbrecv2 多了一個是否 received 的 bool 值
總結下:
對於接收值的 case 會有兩種處理方式,這兩種,區別在於是否將 received 的 bool 值傳送給呼叫方
4、多個 case 的場景
多個 case 的場景
1、會將其中所有 case 轉化為 scase 結構體;
2、呼叫執行時函式 selectgo 選取觸發的 scase 結構體;
3、通過 for 迴圈生成一組 if 語句,來判斷是否選中 case;
這裡來看下 selectgo 的實現
這裡看下函式的幾個引數
cas0:為 scase 陣列的首地址,selectgo() 就是從這些 scase 中找出一個返回;
order0:為一個兩倍 cas0 陣列長度的 buffer,儲存 scase 隨機序列 pollorder 和 scase 中 channel 地址序列 lockorder,陣列前一半是 pollorder,後一半用來 lockorder;
-
pollorder:每次 selectgo 執行都會把 scase 序列打亂,以達到隨機檢測 case 的目的;
-
lockorder:所有 case 語句中 channel 序列,以達到去重防止對 channel 加鎖時重複加鎖的目的;
pc0:對於競態檢測器構建,pc0 指向一個陣列型別[ncases]uintptr
(也在棧上);對於其他版本,它設定為 nil;
nsends: 傳送的 case 的個數;
nrecvs: 接收的 case 的個數;
block: 表示是否存在 default,沒有 default 就表示 select 是阻塞的。
看下返回的資料
int: 選中case的編號,這個case編號跟程式碼一致;
bool: 是否成功從channle中讀取了資料,如果選中的case是從channel中讀資料,則該返回值表示是否讀取成功。
具體的實現邏輯
1、打亂 scase 的順序,鎖定 scase 語句中所有的 channel;
2、按照隨機順序檢測 scase 中的 channel 是否ready;
-
2.1 如果 case 可讀,則讀取 channel 中資料,解鎖所有的 channel,然後返回
(case index, true)
-
2.2 如果 case 可寫,則將資料寫入 channel,解鎖所有的channel,然後返回
(case index, false)
-
2.3 所有 case 都未 ready,並且有 default 語句,則解鎖所有的channel,然後返回
(default index, false)
3、所有 case 都未 ready,且沒有 default 語句
-
3.1 將當前協程加入到所有 channel 的等待佇列
-
3.2 當將協程轉入阻塞,等待被喚醒
4、喚醒後返回 channel 對應的case index
-
4.1 如果是讀操作,解鎖所有的 channel,然後返回
(case index, true)
-
4.2 如果是寫操作,解鎖所有的 channel,然後返回
(case index, false)
這裡來分析下 selectgo 的具體實現
1、打亂 case 的順序
// https://github.com/golang/go/blob/release-branch.go1.16/src/runtime/select.go#L121
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {
...
// 生成隨機順序
norder := 0
for i := range scases {
cas := &scases[i]
// 忽略輪詢和鎖定命令中沒有通道的情況
if cas.c == nil {
cas.elem = nil // allow GC
continue
}
j := fastrandn(uint32(norder + 1))
pollorder[norder] = pollorder[j]
pollorder[j] = uint16(i)
norder++
}
pollorder = pollorder[:norder]
lockorder = lockorder[:norder]
// 根據 channel 地址進行排序,決定獲取鎖的順序
for i := range lockorder {
j := i
// Start with the pollorder to permute cases on the same channel.
c := scases[pollorder[i]].c
for j > 0 && scases[lockorder[(j-1)/2]].c.sortkey() < c.sortkey() {
k := (j - 1) / 2
lockorder[j] = lockorder[k]
j = k
}
lockorder[j] = pollorder[i]
}
...
// 鎖定選中的 channel
sellock(scases, lockorder)
...
}
select 中的多個 case 是隨機觸發執行的,一次只有一個 case 得到執行。如果我們按照順序依次判斷,那麼後面的條件永遠都會得不到執行,而隨機的引入就是為了避免飢餓問題的發生。
所以可以看到上面會將 scase 序列打亂,以達到隨機檢測 case 的目的,然後記錄到 pollorder 中。
2、找出已經 ready 的 case
// https://github.com/golang/go/blob/release-branch.go1.16/src/runtime/select.go#L121
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {
...
var (
gp *g
sg *sudog
c *hchan
k *scase
sglist *sudog
sgnext *sudog
qp unsafe.Pointer
nextp **sudog
)
// pass 1 - 遍歷所有 scase,確定已經準備好的 scase
var casi int
var cas *scase
var caseSuccess bool
var caseReleaseTime int64 = -1
var recvOK bool
// 因為上面已經將scases隨機寫入到pollorder中
// 所以這裡的遍歷相比於原 cas0的順序,就是隨機的
for _, casei := range pollorder {
casi = int(casei)
cas = &scases[casi]
c = cas.c
// 接收資料
if casi >= nsends {
// 有 goroutine 等待傳送資料
sg = c.sendq.dequeue()
if sg != nil {
goto recv
}
// 緩衝區有資料
if c.qcount > 0 {
goto bufrecv
}
// 通道關閉
if c.closed != 0 {
goto rclose
}
// 傳送資料
} else {
if raceenabled {
racereadpc(c.raceaddr(), casePC(casi), chansendpc)
}
// 判斷通道的關閉情況
if c.closed != 0 {
goto sclose
}
// 接收等待佇列有 goroutine
sg = c.recvq.dequeue()
if sg != nil {
goto send
}
// 緩衝區有空位置
if c.qcount < c.dataqsiz {
goto bufsend
}
}
}
// 如果不阻塞,意味著有 default,準備退出select
if !block {
selunlock(scases, lockorder)
casi = -1
goto retc
}
...
bufrecv:
// 可以從 buffer 接收
if raceenabled {
if cas.elem != nil {
raceWriteObjectPC(c.elemtype, cas.elem, casePC(casi), chanrecvpc)
}
racenotify(c, c.recvx, nil)
}
if msanenabled && cas.elem != nil {
msanwrite(cas.elem, c.elemtype.size)
}
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:
// 可以傳送到 buffer
if raceenabled {
racenotify(c, c.sendx, nil)
raceReadObjectPC(c.elemtype, cas.elem, casePC(casi), chansendpc)
}
if msanenabled {
msanread(cas.elem, c.elemtype.size)
}
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
recv:
// 可以從一個休眠的傳送方 (sg)直接接收
recv(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
if debugSelect {
print("syncrecv: cas0=", cas0, " c=", c, "\n")
}
recvOK = true
goto retc
rclose:
// 在已經關閉的 channel 末尾進行讀
selunlock(scases, lockorder)
recvOK = false
if cas.elem != nil {
typedmemclr(c.elemtype, cas.elem)
}
if raceenabled {
raceacquire(c.raceaddr())
}
goto retc
send:
// 可以向一個休眠的接收方 (sg) 傳送
if raceenabled {
raceReadObjectPC(c.elemtype, cas.elem, casePC(casi), chansendpc)
}
if msanenabled {
msanread(cas.elem, c.elemtype.size)
}
send(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
if debugSelect {
print("syncsend: cas0=", cas0, " c=", c, "\n")
}
goto retc
retc:
if caseReleaseTime > 0 {
blockevent(caseReleaseTime-t0, 1)
}
return casi, recvOK
sclose:
// 向已關閉的 channel 進行傳送
selunlock(scases, lockorder)
panic(plainError("send on closed channel"))
}
1、因為上面已經將 scases 隨機寫入到 pollorder 中,所以這裡的遍歷相比於原 cas0 的順序,就是隨機的;
2、case 監聽的 channel 有兩種操作,讀取或者寫入;
讀取資料
-
1、如果有傳送的 goroutine 在等待資料的接收,那麼直接從這個 goroutine 中讀出資料,結束 select;
-
2、如果 channel 的緩衝區有資料,在緩衝去讀出資料, 結束 select;
-
3、如果 channel 關閉了,讀出零值,結束 select。
所以可看出,已經關閉的 channel ,用 select 是可以讀出資料的。
傳送資料
-
1、如果 channel 關閉了,這時候會觸發 panic,因為已經關閉的 channel 是不能傳送資料的;
-
2、如果 channel 的接收等待佇列有 goroutine,說明有 goroutine ,正在阻塞等待從該 channel 中接收資料,那麼資料直接傳送給該 goroutine,結束 select;
-
3、如果 channel 的緩衝區有資料,傳送到資料到 channel 的緩衝區中,結束 select。
如果傳送的 channel 中沒有快取空間,接收 channel 的快取空間為空。這時候該 select 將會阻塞。
如果有 block 為 false ,就表示 select 中有 default,然後執行 default 結束 select。
如果 block 為 true 表示沒有 default,需要在阻塞 select,細節見下文。
3、case 都沒 ready,且沒有 default
// https://github.com/golang/go/blob/release-branch.go1.16/src/runtime/select.go#L121
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {
...
// pass 2 - 所有 channel 入隊,等待處理
gp = getg()
if gp.waiting != nil {
throw("gp.waiting != nil")
}
nextp = &gp.waiting
for _, casei := range lockorder {
casi = int(casei)
// 獲取一個 scase
cas = &scases[casi]
// 監聽的 channel
c = cas.c
// 構建sudog,設定這一次阻塞傳送的相關資訊
sg := acquireSudog()
sg.g = gp
sg.isSelect = true
// No stack splits between assigning elem and enqueuing
// sg on gp.waiting where copystack can find it.
sg.elem = cas.elem
sg.releasetime = 0
if t0 != 0 {
sg.releasetime = -1
}
sg.c = c
// 按鎖定順序構造等待列表。
*nextp = sg
nextp = &sg.waitlink
if casi < nsends {
c.sendq.enqueue(sg)
} else {
c.recvq.enqueue(sg)
}
}
// goroutine 陷入睡眠,等待某一個 channel 喚醒 goroutine
gp.param = nil
// Signal to anyone trying to shrink our stack that we're about
// to park on a channel. The window between when this G's status
// changes and when we set gp.activeStackChans is not safe for
// stack shrinking.
atomic.Store8(&gp.parkingOnChan, 1)
// 將當前的 Goroutine 陷入沉睡等待喚醒
gopark(selparkcommit, nil, waitReasonSelect, traceEvGoBlockSelect, 1)
gp.activeStackChans = false
sellock(scases, lockorder)
gp.selectDone = 0
sg = (*sudog)(gp.param)
gp.param = nil
...
}
如果 case 都沒有 ready ,並沒有 default
這時候會迴圈構建 sudog 的佇列,並且按鎖定順序構造等待列表,附在 goroutine 中,然後使用 gopark 掛起當前 goroutine 等待排程器的喚醒。
4、喚醒後返回 channel 對應的 case
// https://github.com/golang/go/blob/release-branch.go1.16/src/runtime/select.go#L121
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {
...
casi = -1
cas = nil
caseSuccess = false
sglist = gp.waiting
// 在從 gp.waiting 取消連結之前清除所有元素。
for sg1 := gp.waiting; sg1 != nil; sg1 = sg1.waitlink {
sg1.isSelect = false
sg1.elem = nil
sg1.c = nil
}
gp.waiting = nil
for _, casei := range lockorder {
k = &scases[casei]
if sg == sglist {
// sg has already been dequeued by the G that woke us up.
casi = int(casei)
cas = k
caseSuccess = sglist.success
if sglist.releasetime > 0 {
caseReleaseTime = sglist.releasetime
}
} else {
c = k.c
if int(casei) < nsends {
c.sendq.dequeueSudoG(sglist)
} else {
c.recvq.dequeueSudoG(sglist)
}
}
sgnext = sglist.waitlink
sglist.waitlink = nil
releaseSudog(sglist)
sglist = sgnext
}
...
selunlock(scases, lockorder)
goto retc
...
retc:
if caseReleaseTime > 0 {
blockevent(caseReleaseTime-t0, 1)
}
return casi, recvOK
...
}
遍歷全部 case 時,先獲取當前 Goroutine 接收到的引數 sudog 結構,然後依次對比所有 case 對應的 sudog 結構找到被喚醒的 case,獲取該 case 對應的索引並返回。
因為已經找到了一個可執行的 case,剩下的 case 中沒有被用到的 sudog 就會被忽略並且釋放掉。為了不影響 Channel 的正常使用,我們還是需要將這些廢棄的 sudog 從 Channel 中出隊。
總結
1、空的 select 會發生死鎖;
2、select 中的 case 分支的執行是隨機的;
3、如果沒有 default 分支
如果沒有 default 分支,select 將會一直處於阻塞狀態,直到其中的一個 case 就緒;
4、如果有 default 分支
如果有 default 分支,隨機將 case 分支遍歷一遍,如果有 case 分支可執行,處理對應的 case 分支;
如果遍歷完 case 分支,沒有可執行的分支,執行 default 分支。
5、select 中向 channel 的傳送不會阻塞 select;
6、select 語句中讀操作要判斷是否成功讀取,關閉的 channel 也可以讀取。
參考
【Select 語句的本質】https://golang.design/under-the-hood/zh-cn/part1basic/ch03lang/chan/#select-
【select】https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-select/#52-select
【go原始碼閱讀之Select】https://nercoeus.github.io/2020/01/13/go原始碼閱讀之Select/
【GO專家程式設計】https://book.douban.com/subject/35144587/
【深入瞭解下 go 中的 select】https://github.com/boilingfrog/Go-POINT/blob/master/golang/select/select原始碼閱讀.md
【select原始碼閱讀】https://boilingfrog.github.io/2022/04/16/go中select原始碼閱讀/