由于 select 是专门支持 channel 的收发操作的,那么首先应该了解 channel 的结构。
channel 的底层数据结构
type hchan struct {
qcount uint
dataqsiz uint
buf unsafe.Pointer
elemsize uint16
closed uint32
elemtype *_type
sendx uint
recvx uint
recvq waitq
sendq waitq
lock mutex
}
type waitq struct {
first *sudog
last *sudog
}
看起来字段挺多,实际上可以这么分类
由此看来,channel 维护一个数据缓冲区,该缓冲区通过循环队列实现;维护对该 channel 进行收发操作而被阻塞的 goroutine 链表。
channel 的特点:
- 先进先出,先发送到 channel 中的数据会先被读取,原因是缓冲区是通过循环队列实现的;因发送数据而先阻塞的 goroutine 在缓冲区有空位时会先被唤醒,同理,因接收数据而先阻塞的 goroutine 会在缓冲区有数据时先被唤醒,原因是 channel 通过双向链表组织阻塞的 goroutine。
- 向已关闭的 channel 发送数据会直接 panic,底层通过 hchan 中 closed 字段的值来判断 channel 是否已关闭,0 表示未关闭,其他值表示已关闭。
- 读已关闭的 channel 会收到元素的默认零值和 false 标志。
- 关闭已关闭的 channel 或值为 nil 的 channel 会直接 pannic。
select 的底层数据结构
type scase struct {
c *hchan // chan
elem unsafe.Pointer // data element
}
select 支持多 channel 收发操作,因此用字段 c 存储相关的 channel
select 特点:
- 一次计算。select 会计算完所有 case 的情况后执行一次。
- 非阻塞收发。当有 default 时,没有 case 就绪,就默认执行 default 语句。
- 阻塞收发。无 default 时,没有 case 就绪,阻塞等待直至有 case 就绪。
- 随机执行。多个 cases 就绪时,随机执行其中一个。底层通过引入随机数使得轮询顺序随机化,保证公平性,避免某个 case 饥饿。