概述
在 Go 里面,有个很常用的 select 语句,用平时都会用,但是你知道它底层的实现机制吗?我不知道,所以我就学习了一下,顺便记录一下结论。
channel 的工作机制
在开始说 select 之前,先来收一下 unbuffer 的 channel 是怎么工作的,其实 buffer channel 在 buffer 满的时候也是类似的机制,那就先说 unbuffer 吧。
在 channel 的内部,有两个数据结构,分别是 sendq
和 recvq
,他们用于放置因为发送消息到 channel 或者从 channel 中读取消息而阻塞的 goroutine;当然,对于发送消息而阻塞的 goroutine 除了存放 goroutine 指针之外,还会存放发送的消息指针。
如果一个有阻塞的 recv goroutine 遇到一个发送来的消息的时候,channel 就会从 recvq
中出队一个接收 goroutine,然后通过 memmove
将消息 copy 给他,从而完成一个 channel 的消息交换,内部数据结构类似于:
对于 buffer channel,其实是多了一个 buffer 的存储,在 Go 里面,buffer 的存储是类似于一个环状队列实现,在 channel 结构中有一下属性:
- buffer:一个固定长度的数组
- qcount:当前 buffer 中有多少个元素
- dataqsiz:buffer 的长度
- sendx:下一个存储元素的位置
- recvx:下一个返回元素的位置
通过这几个变量,就可以实现环形数组了。
select 的实现
当了解完 channel 的实现之后,再来理解 select 就比较容易一些了,例如这一段代码:
[root@liqiang.io]# cat main.go
select {
case <- a:
case <- b:
case <- c:
}
当 a、b 和 c channel 都阻塞的时候,在 Go 的实现里面,其实就是在 a、b 和 c 三个 channel 的等待队列里面都将 select 这个 goroutine 注册上,当这三个中只要有一个返回了之后,select 对应的 goroutine 就会得到激活并运行,只不过 Go 需要额外地完成其他未激活 channel 的出队列操作。
但是,Go 在某些条件下,会简化问题,例如下面这段带 default 的代码:
[root@liqiang.io]# cat default.go
select {
case <- a:
default:
}
如果 channel a 是阻塞的,其实 select 就不用往 channel 的等待队列中插入 goroutine 了,因为这个时候直接执行 default 就可以了,所以 Go 的简化逻辑就是:对于这种带 default 的 select,直接检查对应的 channel 是否有数据,如果没有直接执行 default,有数据,直接获取数据,并运行对应的代码。
还有一种更简单的逻辑,Go 的简化也不一样:
[root@liqiang.io]# cat single.go
select {
case <- a:
}
对于这种只有一个 channel 的,其实就是我们平时使用 channel 的简单方式,所以可以直接简化成:
[root@liqiang.io]# cat single2.go
<- a