由于 select 是专门支持 channel 的收发操作的,那么首先应该了解 channel 的结构。

channel 的底层数据结构

  1. type hchan struct {
  2. qcount uint
  3. dataqsiz uint
  4. buf unsafe.Pointer
  5. elemsize uint16
  6. closed uint32
  7. elemtype *_type
  8. sendx uint
  9. recvx uint
  10. recvq waitq
  11. sendq waitq
  12. lock mutex
  13. }
  1. type waitq struct {
  2. first *sudog
  3. last *sudog
  4. }

看起来字段挺多,实际上可以这么分类
select   channel - 图1
image.png
微信图片_20210809202300.jpg
由此看来,channel 维护一个数据缓冲区,该缓冲区通过循环队列实现;维护对该 channel 进行收发操作而被阻塞的 goroutine 链表。

channel 的特点:

  1. 先进先出,先发送到 channel 中的数据会先被读取,原因是缓冲区是通过循环队列实现的;因发送数据而先阻塞的 goroutine 在缓冲区有空位时会先被唤醒,同理,因接收数据而先阻塞的 goroutine 会在缓冲区有数据时先被唤醒,原因是 channel 通过双向链表组织阻塞的 goroutine。
  2. 向已关闭的 channel 发送数据会直接 panic,底层通过 hchan 中 closed 字段的值来判断 channel 是否已关闭,0 表示未关闭,其他值表示已关闭。
  3. 读已关闭的 channel 会收到元素的默认零值和 false 标志。
  4. 关闭已关闭的 channel 或值为 nil 的 channel 会直接 pannic。

select 的底层数据结构

  1. type scase struct {
  2. c *hchan // chan
  3. elem unsafe.Pointer // data element
  4. }

select 支持多 channel 收发操作,因此用字段 c 存储相关的 channel

select 特点:

  1. 一次计算。select 会计算完所有 case 的情况后执行一次。
  2. 非阻塞收发。当有 default 时,没有 case 就绪,就默认执行 default 语句。
  3. 阻塞收发。无 default 时,没有 case 就绪,阻塞等待直至有 case 就绪。
  4. 随机执行。多个 cases 就绪时,随机执行其中一个。底层通过引入随机数使得轮询顺序随机化,保证公平性,避免某个 case 饥饿。