Go 深入源码 —— select 与 channel

Iceber · · 619 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

> Golang 通过 `select...case` 语句实现了对 `channel` 的多路复用以及非阻塞收发操作 本文将着重讲解以下问题: * 编译器对 `select` 语句的优化 * [`select` 如何随机选择 `case`](http://icebergu.com/archives/go-select-channel#%E5%A6%82%E4%BD%95%E9%9A%8F%E6%9C%BA%E9%80%89%E6%8B%A9-case) * [当多个 `channel` 可以进行收发操作时,`select` 又会如何选择 `case`](http://icebergu.com/archives/go-select-channel#%E5%A4%9A%E4%B8%AAchannel%E5%90%8C%E6%97%B6%E5%87%86%E5%A4%87%E8%BF%9B%E8%A1%8C%E6%94%B6%E5%8F%91%E6%93%8D%E4%BD%9C%E5%A6%82%E6%9E%9C%E8%A7%A3%E5%86%B3%E7%AB%9E%E4%BA%89%E9%97%AE%E9%A2%98) > **强烈建议阅读本文前,先看关于 channel 的源码解析 [Go 深入源码 —— Channel](http://icebergu.com/archives/go-channel)** > > 如果只关心 select 如何实现 channel 的多路复用,以及如何随机选择 `case` 的话可以直接看 [selectgo 实现 channel 的多路复用](http://icebergu.com/archives/go-select-channel#selectgo-%E5%AE%9E%E7%8E%B0-channel-%E7%9A%84%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8) ## select 使用与编译阶段的转换 ### 无 case,永久阻塞 ```go select{} ``` 没有任何 case 的 `select` 语句会被编译器转换为`runtime.block()`函数,永久阻塞 ##### 编译阶段转换 ```go // src/cmd/compile/internal/gc/select.go func walkselectcases(caes *Nodes) []*Node { // 获取 case 操作的数量 n := cases.Len() if n == 0 { return []*Node{mkcall("block", nil, nil)} } // ... } // src/runtime/select.go func block() { gopark(nil, nil, waitReasonSelectNoCases, traceEvGoStop, 1) // 永久阻塞 } ``` ### 单一 channel,阻塞收发 ```go ch := make(chan struct{}) select { case data <- ch: fmt.Printf("ch data: %v\n", data) } // 编译器转换 if ch == nil { block() } data := <- ch ``` 只有一个 channel 操作,实际会被编译器转换为相应channel 相应的收发操作,其实和实际调用 `data := <- ch` 并没有什么区别 ##### 编译阶段转换 ```go // src/cmd/compile/internal/gc/select.go func walkselectcases(caes *Nodes) []*Node { // 获取 case 操作的数量 n := cases.Len() // ... if n == 1 { if cas.Left != nil { // not default n := cas.Left // n 表示 case 的 channel 操作 // ... // 这一段操作便是转换成 if ch == nil {block()}; n; a := nod(OIF, nil, nil) a.Left = nod(OEQ, ch, nodnil()) var ln Nodes ln.Set(l) a.Nbody.Set1(mkcall("block", nil, &ln)) l = ln.Slice() l = append(l, a, n) } // ... return l } } ``` ### 单一 channel + default,非阻塞收发 ```go ch := make(chan int) select { case ch <- 1: // ... default: fmt.Println("do default") } // 编译器转换 if selectnbsend(ch, 1) { // ... } else { fmt.Println("do default") } ``` 编译器会将 channel 的收发操作转换成 `selectnbsend` 或者 `selectnbrecv`/`slectnbrecv2`函数来完成非阻塞操作 ##### 编译阶段转换 ```go // src/cmd/compile/internal/gc/select.go func walkselectcases(caes *Nodes) []*Node { // 获取 case 操作的数量 n := cases.Len() // ... // 两个 case操作,其中一个还是 default if n == 2 && (cases.First().Left == nil || cases.Second().Left == nil { n := cas.Left // n 是 case 的 channel 操作 r := nod(OIF, nil, nil) switch n.Op { case OSEND: ch := n.Left // 转换为 if selectnbsend(c, v){ //... } else { // default body ...} r.Left = mkcall1(chanfn("selectnbsend", 2, ch.Type), types.Types[TBOOL], &r.Nint, ch, n.Right) case OSELRECV: // 转换为 if selectnbrecv(&v, c){ //... } else { // default body ...} // ... case OSELRECV2: // 转换为 if selectnbrecv2(&v, &received, c){ //... } else { // default body ...} // ... } return } } ``` channel 的非阻塞收发实际都是调用的 [`chansend`](http://icebergu.com/archives/go-channel#%E6%B7%B1%E5%85%A5%E6%BA%90%E7%A0%81) 或者 [`chanrecv`](http://icebergu.com/archives/go-channel#%E6%B7%B1%E5%85%A5%E6%BA%90%E7%A0%81-1) ```go // src/runtime/chan.go // 非阻塞发送 func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) { // block 参数为 false,非阻塞调用 return chansend(c,elem, false, getcallerpc()) } // 非阻塞接收 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 } ``` 而且 channel 会对于非阻塞收发操作有一些优化 ```go // 非阻塞操作, block 参数为 false func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { // ... // 快速检测,非阻塞时,有些情况不需要获取锁就可以直接返回 // 非阻塞,未关闭,非缓冲+没有等待接收的 goroutine 或者 缓冲+缓冲区已满 if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) || ((c.dataqsiz < 0 && c.qcount == c.dataqsiz)) { // 返回 false,表示未发送成功 return false } lock(&c.lock) } func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool){ // ... // 快速检测,在非阻塞模式下,和发送一样有些条件不需要加锁就可以直接判断返回 // 非阻塞并且未关闭,非缓冲+没有待发送者或者有缓冲+缓冲为空 if !block && (c.dataqsiz == 0 && c.sendq.first == nil || c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) && atomic.Load(&c.closed) == 0 { return } lock(&c.lock) } ``` 在加锁前对 channel 进行判断,较少了加锁带来的性能问题 具体的 channel 收发操作,可以参考 [Go 深入源码 —— Channel](http://icebergu.com/archives/go-channel) ### 多 channel,实现多路复用 ```go ch1 := make(chan int) ch1 := make(chan int) select { case data, ok := <- ch1: fmt.Println("recv from ch1, data: %v, ok: %v", data, ok) case ch2 <- 1: fmt.Println("send to ch2") } // 编译器转换 // 第一阶段 生成 scase 对象数组 selv := [2]scase{} order := [2*2]uint16 for i, case := range cases { c := cases{} c.kind = ... c.c = ... c.elem = ... } // 第二阶段 编译器生成调用 selectgo 的逻辑 chosen, recv := selectgo(selv, order, 2) // 第三阶段 根据 selectgo 返回值 chosen 来生成 if 语句来执行相应索引的 case if chosen == 0 { // ... } if chosen == 1 { // ... } ``` 多 case 的 `select` 语句实现了 channel 的多路复用,select 会阻塞,直到有 case 操作完成了收发操作 **如果有多个 case 操作已经可以执行收发操作了,但是 select 会随机选择一个 case 完成收到操作,然后执行 case 下的相应逻辑** 编译器实际会将 select 语句转换成 `selectgo` 函数,每个 case 操作都会被转换成 `scase` 结构,然后作为参数传递给 `selectgo` ##### 编译阶段转换 ```go // src/cmd/compile/internal/gc/select.go func walkselectcases(cases *Nodes) []*Node { // 获取 case 操作的数量 n := cases.Len() if n == 0 { // 无 case // ... } if n == 1 { // 单一 case // ... } if n == 2 && (cases.First().Left == nil || cases.Second().Left == nil) { // 单一 case + defult // ... } // 后续逻辑便是多 case 的情况了 // selv scase 数组,scasetype() 返回的便是 scase // selv 和 order 会作为 selectgo 的参数 selv := temp(types.NewArray(scasetype(), int64(n))) order := temp(types.NewArray(types.Types[TUINT16], 2*int64(n))) // 第一阶段 // 遍历 case 生成 scase 对象存放到 selv 中 for i, cas := range cases.Slice() { var c, elem *Node var kind int64 = caseDefault // cas.Left 如果不为 nil,那么说明是 channel 的收发操作 if n := cas.Left; n != nil { switch n.Op { case OSEND: kind = caseSend c = n.Left elem = n.Right case OSELRECV, OSELRECV2: kind = caseRecv c = n.Left elem = n.Right } } // 根据 c, elem 构造 scase,并存放到 selv 中 // ... } // selectgo 会返回的两个值,chosen 表示被选中的 case 的索引,recvOK 表示对于接收操作,是否成功接收 chosen := temp(types.Types[TINT]) recvOK := temp(types.Types[TBOOL]) // 第二阶段 // chose, recvOK := selectgo(selv, order, n) r = nod(OAS2, nil, nil) r.List.Set2(chose, recvOK) fn := syslook("selectgo") r.Rlist.Set1(mkcall1(fn, fn.Types, Results(), nil, bytePtrToIndex(selv, 0), bytePtrToIndex(order, 0), nodeintconst(int64(n)))) // 第三阶段 // 根据 selectgo 返回的 case 索引生成多个 if 语句 for i, cas := range cases.Slice() { // 转换成if chosen == i { // ... case body } cond := nod(OEQ, chosen, nodintconst(int64(i))) r = node(OIF, cond, nil) // ... r.Nbody.AppendNodes(&cas.NBody) } } ``` **在分配 `selv` 和 order` 时,使用 `temp` 方法,应该是为了保证数据会分配到栈中而不是堆中** ### 多 channel + default ```go ch1 := make(chan int) ch1 := make(chan int) select { case data, ok := <- ch1: fmt.Println("recv from ch1, data: %v, ok: %v", data, ok) case ch2 <- 1: fmt.Println("send to ch2") default: fmt.Println("do default") } ``` 多 case + default 同样也是调用 selectgo 函数,`default` 也会封装成 kind 为 caseDefault 的 `scase` 对象的 **`selectgo` 并不会进行阻塞,如果所有 case 的 channel 都无法立即完成收发操作,那就会直接执行 default 操作** ## selectgo 实现 channel 的多路复用 对于 select 语句,编译器会进行相应的转换优化操作,而多 channel 操作便是去调用 `selectgo` 函数来实现 ### scase select 中 case 操作,无论是 channel 收发还是 default 都会被转换为 `scase` 结构 ```go const ( caseNil = iota // 表示 channel 为 nil 的情况 caseRecv caseSend caseDefault ) type scase struct { c *hchan //进行收发操作的 channel elem unsafe.Pointer // 收发的数据源 kind uint16 // 上述的四种 kind ... } ``` scase 结构很简单,主要记录了 case 操作的类型,channel,以及 channel 收发的数据源 ### selectgo 现在我们开始分析 select 的重头戏 `selectgo` 函数 ```go func selectgo(cas0 *scase, order0 *uint16, ncase int)(int, bool) ``` * `cas0` 指向一个类型为 [ncases]scase 的数组 * `order0` 是一个指向[2*ncases]uint16,数组中的值都是 0 * `selectgo` 会返回选中的序号,如果是个接收操作,还会返回是否接收到一个值 如果看过[上文](http://icebergu.com/archives/go-select-channel#%E7%BC%96%E8%AF%91%E9%98%B6%E6%AE%B5%E8%BD%AC%E6%8D%A2-3)的话就会知道,编译器会将 scases 数组,还有用于排序的数组和 case 数量一起传给 selectgo > 为什么 `selectgo` 还需要传递一个 order0,而不是直接根据 ncase 直接分配呢 > 编译转换会使用 `temp` 函数来构造生成数组的语句,而这个语句便可以保证数据会分配到栈上,而不是堆上,避免了不必要的堆分配 而 select 做的第一件事便是将 case0,order0 这些指针转换成相应的 slice 结构 ``` cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0)) order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0)) // [:n:n] 的方式会让slice 的 len 和 cap 相等 scases := cas1[:ncases:ncases] pollorder := order1[:ncases:ncases] lockorder := order1[ncases:][:ncases] // 对于 channel 为 nil 的收发操作,他们的 kind 被修改为 caseNil for i := range scases { case := &scases[i] if cas.c == nil && cas.kind != caseDefault { *cas = scase{} } } ``` order1 会被分为 `pollorder` 和 `lockorder`,而这两个slice将会真正决定 `select` 的随机选择以及死锁问题 ##### 如何随机选择 case **`selectgo` 是通过循环 `scases` 来挑选可以收发的 channel** **然而循环时并不是按照 `scases`的顺序,而是 `pollorder` 中记录的顺序, 这样可以避免 channel 的饥饿问题** **为了保证 `select` 随机选择 case,所以使用 `fastrandn` 来生成随机数** ```go for i := 1; i < ncases; i++ { j := fastrandn(uint32(i+1)) pollorder[i] = pollorder[j] pollorder[j] = uint16(i) } ``` `pollorder` 在开始的时候值都是 0,循环结束后值便是随机顺序的 scases 索引 ##### 避免相同 channel 重复加锁,以及死锁问题 **`selectgo` 在查找 scases 中已经可以进行收发操作的 channel 前会先对所有的 channel 进行加锁操作** ###### 死锁问题 > 如果多个 goroutine 都需要锁定 ch1 ch2,而他们加锁的顺序不固定,那么很可能会出现死锁问题 > 这个时候,对加锁的顺序就有要求了,按照同样的顺序的话,没有竞争到 ch1.lock 的 goroutine,会等待加锁 ch1.lcok,而不会直接去加锁 ch2.lock **加锁前首先会对 `lockorder` 进行堆排序,生成由 case.c(\*hchan) 来排序的 scases 索引顺序** ```go func selectgo(cas0 *scase, order0 *uint16, ncase int)(int, bool) { ... // ... 对 looporder 堆排序 // selectgo 在查找 scases 前,先对所有 channel 加锁 sellock(scases, lockorder) ... } ``` **`sellock` 对地址相同的 channel 只会加锁一次** ```go func sellock(scases []scases, lockorder []int16) { var c *hchan for _, o := range lockorder { c0 := scases[0].c // 根据加锁顺序获取 case // c 记录了上次加锁的 hchan 地址,如果和当前 *hchan 相同,那么就不会再次加锁 if c0 != nil && c0 != c { c = c0 lock(&c.lock) } } } ``` 加锁完成后,可以进入 `selectgo` 主循环逻辑了 主逻辑会分为三部分: 1. 首先根据 `pollorder` 的顺序查找 scases 是否有可以立即收发的 channel 2. channel 都没有准备好,并且不存在 default,那么就将当前 `goroutine` 加入到 channel 相应的等待队列,然后等待收其他 `goroutine` 唤醒 3. 被唤醒后,再次找到满足条件的 channel ##### 查找 scases 中是否有准备好的 channel ```go loop: for i := 0; i < ncases; i++ { // 根据 `pollorder` 记录的随机 scases 索引来获取 cas casi = int(pollorder[i]) cas = &scases[casi] c = cas.c switch case.kind { case caseNil: continue case caseRecv: // ... case caseSend: // ... case caseDefault: // ... } } // ... } ``` 根据 `pollorder` 记录的随机 `scases` 索引来遍历处理 case,然后根据 `case.kind` 来查看 channel 是否准备好,然后 `goto` 跳转到相应逻辑 `case.kind` 为 `caseNil`,说明 channel 为 nil,那么 `continue`,不进行任何处理 ###### caseRecv: channel 接收操作 ```go switch case.kind { case caseRecv: sg = c.sendq.dequeue() if sg != nil { goto recv } if c.qcount > 0 { goto bufrecv } if c.closed != 0 { goto rclose } ... } ``` 如果 channel 中有待发送的 goroutine, 跳转到 `recv`,调用 [`recv`](https://github.com/golang/go/blob/master/src/runtime/chan.go#L602)完成接收操作 ```go recv: // src/runtime/chan.go recv(c, sg, cas.elem, func() { selunlock(cases, lockorder) }, 2) recvOK = true goto retc ``` 如果 channel 中有缓冲数据,那么跳转到 `bufrecv`,从缓冲区中获取数据 ```go bufrecv: recvOK = true gp = chanbuf(c, c.recx) if cas.elem != nil { typedmemclr(c.elemtype, gp) } // ... channel 缓冲区调整 selunlock(scases, lockorder) goto retc ``` 如果 channel 已关闭,跳转到 `rclose`, 将接收值置为空值,recvOK 置为 false ```go rclose: selunlock(scases, lockorder) if cas.elem != nil { typedmemclr(c.elemtype, cas.elem) } goto retc ``` 接收操作与正常接收操作类似,可以参考 [channel 接收数据](http://icebergu.com/archives/go-channel#%E6%B7%B1%E5%85%A5%E6%BA%90%E7%A0%81-1) ###### caseSend: channel 发送操作 ```go switch case.kind { case caseSend: if c.closed != 0 { goto sclose } sg = c.recvq.dequeue() if sg != nil { goto send } if c.qcount < c.dataqsiz { goto bufsend } ... } ``` 对于发送操作会先判断 channel 是否已经关闭,跳转到 `sclose`,直接 panic ```go sclose: selunlock(scases, lockorder) panic(plainError("send on closed channel")) ``` 如果 channel 为关闭,并且有待接收队列不为空,说明 channel 的缓冲区为空,跳转到 send , 调用 `send` 函数,直接发送数据给待接收者 ```go send: send(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2) goto retc ``` 如果缓冲区不为空的话,跳转到 `bufsend`,从缓冲区获取数据 ```go bufsend: typedmemove(c.elemtype, chanbuf(c, c.sendx), cas.elem) // ... 调整缓冲区索引 selunlock(scases, lockorder) goto retc ``` ###### caseDefult: default ```go var dfli int var dfl *scase for i := 0; i < ncases; i++{ // ... switch case.kind { ... case caseDefault: dfli = casi dfl = cas } } if dfl != nil { selunlock(scases, lockorder) casi = dfli cas = dfl return casi, recvOK } ``` `dfli` 和 `dfl` 记录了 kind 为 `caseDefault` 的 case 如果所有 channel 都没有完成收发操作,那么就不会执行 `goto` 跳转,循环结束后判断 `dfl != nil` 存在 default,就直接返回 default 的索引 `dfli` ##### 加入 channel 等待队列,等待被唤醒 如果没有 channel 可以执行收发操作,并且没有 `default case`,那么就将当前 `goroutine` 加入到 channel 相应的收发队列中,等待被其他 goroutine 唤醒 ```go func selectgo(cas0 *scase, order0 *uint16, ncase int)(int, bool) { // ... setlock(scases, lockorder) // ... 查看是否有准备好的 channel,或者存在 default case gp = getg() for _, casi := range lockorder { casi = int(casei) cas = &scases[casi] if cas.kind == caseNil { continue // channel 为 nil 直接跳过 } c = cas.c // 构造 sudog sg := acquireSudog() sg.g = gp sg.isSelect = true sg.elem = cas.elem sg.c = c // 加入相应等待队列 switch cas.kind { case caseRecv: c.recvq.enqueue(sg) case caseSend: c.sendq.enqueue(sg) } } // 被唤醒后会根据 param 来判断是否是由 close 操作唤醒的,所以先置为 nil gp.param = nil // selparkcommit 会解锁所有 channel gopark(selparkcommit, nil,waitReasonSelect, traceEvGoBlockSelect, 1) ``` 将当前 goroutine 加入到每一个 channel 等待队列中 **如果所有的 channel 都是 nil 的话就会被永久阻塞,不会被唤醒** ###### 多个channel同时准备进行收发操作,如果解决竞争问题 **`selectgo` 构建出来的 `sudog` 会将 `isSelect` 置为 true,这样时为了避免多个 channel 从等待队列中获取相同 goroutine 封装的 `sudog`** 现在来看一下 等待队列的出队方法 `dequeue` ```go struct hchan { ... sendq *waitq recvq *waitq } func (q *waitq) dequeue() *sudog { for { sgp := q.first if sgp == nil { return nil } // ... // 原子操作,如果 spg.g.selectDone 不为 1,则修改为 0 // 为 1 说明 spg.g 已经被其他 channel 取出,直接跳过 sgp if sgp.isSelect && !atomic.Cas(spg.g.selectDone, 0, 1) { continue } } return sgp } ``` ##### 被唤醒,选择合适的 case ``` func selectgo(cas0 *scase, order0 *uint16, ncase int)(int, bool) { // ... gopark(selparkcommit, nil,waitReasonSelect, traceEvGoBlockSelect, 1) // 加锁所有的 channel sellock(scases, lockorder) // param 存放唤醒 goroutine 的 sudog,如果是关闭操作唤醒的,那么就为 nil sg = (*sudog)(pg.param) gp.param = nil // waiting 链表按照 lockorder 顺序存放着 sudog sglist = gp.waiting casi = -1 cas = nil // cas 便是唤醒 goroutine 的 case for _, casei := range lockorder { k = &scases[casei] if k.kind == caseNil { continue } // 如果相等说明,goroutine 是被当前 case 的 channel 收发操作唤醒的 // 如果是关闭操作,那么 sg 为 nil, 不会对 cas 赋值 if sg == sglist { casi = int(casei) cas = k } else { // goroutine 已经被唤醒,将 sudog 从相应的收发队列中移除 c = k.c // func (q *waitq) dequeueSudoG(sgp *sudog) // dequeueSudoG 会通过 sudog.prev 和 sudog.next 将 sudog 从等待队列中移除 if k.kind == caseSend { c.sendq.dequeueSudoG(sglist) } else { c.recvq.dequeueSudoG(sglist) } } // 释放 sudog,然后准备处理下一个 sudog sgnext = sglist.waitlink sglist.waitlink = nil releaseSudog(sglist) sglist = sgnext } ... } ``` 当前`goroutine` 被唤醒后,将其他 `sudog` 从相应的 channel 等待队列中移除 `selectgo` 会根据变量 `cas` 的值来判断是收发操作唤醒还是关闭操作唤醒 关闭操作唤醒的话 `gp.param` 会被置为 nil,那么就不会赋值 `cas` 变量 ``` func selectgo(cas0 *scase, order0 *uint16, ncase int)(int, bool) { // ... loop: // ... 查找 scases 中是否有准备完成的 channel gopark(selparkcommit, nil,waitReasonSelect, traceEvGoBlockSelect, 1) // 加锁所有的 channel sellock(scases, lockorder) // ... if cas == nil { // 由关闭操作唤醒 goroutine,那么再次回到 loop 处 goto loop } c = cas.c if cas.kind == caseRecv { recvOK = true } selunlock(scases, lockorder) return casi, recvOK } ``` ###### 关闭操作唤醒 selectgo,并不一定会选择该 case 对于关闭操作唤醒,逻辑会回到 `loop` 中再次执行 scases 的检查操作 **关闭操作唤醒 `selectgo` 后,`在完成所有 channel 加锁前又有 channel 准备好收发操作了`,那么在 `loop` 查询时,按照 `pollorder` 随机顺序,可能会选中刚刚准备好的 channel,而不是唤醒 `selectgo` 的 case** ###### 收发操作唤醒 selectgo, 必定会选择该 case **而对于收发操作,已经完成了值的拷贝,必然会选择这个 case,而不会再次去查询** ## 总结 **使用 `单 channel`,`单 channel + default` 时编译器会对 select 进行编译转换,这些转换会带来性能的优化** `多 channel: ` * **首先会对所有的所有的 `channel` 进行加锁,而加锁过程会按照 `channel` 进行排序,避免了死锁的出现,并且不会对 `channel` 重复加锁** * **加锁完成后,会使用随机数来打乱查询的顺序,随机选择 case 避免 `channel` 饥饿,保证公平性** * **如果所有 `channel` 都没有准备好,并且有 `default case`,那么就选择 `default case`** * **如果没有 `default`,那么 `select` 就会阻塞,然后等待被其他 goroutine 唤醒 select** * **关闭操作唤醒 `select` 时,最终选择的并不一定是该 `channel` case,因为会再次使用随机顺序来选择合适的 `channel`** * **收发操作唤醒 `select` 时,必然会选择该 channel case** ## 参考 [Go 语言设计与实现 - select](https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-select/) ## 推荐阅读 [Golang 夜读 第 56 期 channel & select 源码分析](https://github.com/talkgo/night/issues/450)

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

619 次点击  ∙  2 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传