select是Go在语言层面提供的多路I/O复用机制,用于检测多个管道是否就绪( 即可读或可写 ),其特性和管道息息相关。
select只能作用于管道,包括数据的读取和写入,如下面代码所示:
func SelectForChan(c chan string) {
var recv string
send := "Hello"
select {
case recv = <-c:
fmt.Printf("recvied %s \n", recv)
case c <- send:
fmt.Printf("sent %s \n", send)
}
}
在上面的代码中,select拥有两个case语句,分别对应管道的读操作和写操作,至于最终执行哪个case语句,取决于函数传入的管道。
第一种情况,管道没有缓冲区:
func main() {
c:=make(chan string)
SelectForChan(c)
}
此时管道既不能读也不能写,两个case语句均不执行,select陷入阻塞,直接main方法运行的话会出现fatal error: all goroutines are asleep - deadlock!,退出程序,原因是: **在main goroutine线,期望从管道中获得一个数据,而这个数据必须是其他goroutine线放入管道的,但是其他goroutine线都已经执行完了(all goroutines are asleep),那么就永远不会有数据放入管道。所以,main goroutine线在等一个永远不会来的数据,那整个程序就永远等下去了。这显然是没有结果的,所以这个程序就说“算了吧,不坚持了,我自己自杀掉,报一个错给代码作者,我被deadlock了”。**一般这种情况需要避免出现,还有一种方式,就是追加default选项。
第二种情况,管道有缓冲区且还可以存放至少一个数据:
func main() {
c:=make(chan string,1)
SelectForChan(c)
}
此时管道可以进行写数据,写操作对应的case得以执行,执行后输出 sent Hello ,函数退出。
第三种情况,管道有缓冲区,缓冲区中已经放满了数据:
func main() {
c:=make(chan string,1)
c<-"world"
SelectForChan(c)
}
此时管道可以进行读数据,读操作对应的case得以执行,执行后输出 recvied world ,函数退出
第四种情况,管道有缓冲区,缓冲区中只有部分数据且还可以继续存放数据:
func main() {
c:=make(chan string,2)
c<-"world"
SelectForChan(c)
}
此时管道即可以读也可以写,select将随机挑选一个case语句执行,任意一个case语句执行结束后函数退出。
综上所述,select的每个case语句只能操作一个管道,要么写入数据,要么读取数据。鉴于管道的特效,如果管道中没有数据读取操作则会堵塞,如果管道中没有空余的缓冲区则写入操作会堵塞。当select的多个case语句中的管道均堵塞时,整个select语句也会陷入堵塞(没有default语句的情况下,且不是,main函数执行),直到任意一个管道解除堵塞。如果多个case均没有堵塞,那么select将随机挑选一个case执行。
select为Go语言的预留关键字,并非函数,其可以在case语句中声明变量并为变量赋值,看上去就像一个函数一样、
case语句读取管道时,可以最多给两个变量赋值,如下:
func SelectAssign(c chan string) {
select {
case <-c: //0个变量
fmt.Println("0")
case d := <-c: //1个变量
fmt.Printf("1: received %s \n",d)
case d, ok := <-c: //2个变量
if !ok{
fmt.Printf("no data found")
break
}
fmt.Printf("2: received %s \n",d)
}
}
case语句中管道的读操作有两个返回条件,一个是成功读取到数据,二是管道中已没有数据且已被关闭。当case语句中包含两个变量时,第二个变量表示是否成功地读取到了数据。
下面的代码传入一个关闭的管道:
func main() {
c := make(chan string)
close(c)
SelectAssign(c)
}
此时select中的三个case语句都有机会执行,,每个case语句收到的数据都为空,但是第三个case语句中可以感知到管道被关闭,从而不必打印空数据。
select中的dafault语句不能处理管道读写操作,当select的所有case语句都堵塞时,default语句将被执行,如下:
func SelectDefault() {
c := make(chan string)
select {
case <-c:
fmt.Printf("received %s \n")
default:
fmt.Printf("no data found in default \n")
}
}
由于管道没有缓冲区,读操作必然堵塞,然而select含有default分支,select将执行default分支并退出。
另外,default实际上也是特殊的case,它能出现在select的任意位置,但每个select仅能有一个default。
下面列举一些在实际项目中使用select的例子
有时我们启动协程处理任务,并且不希望main函数退出,此时就可以让main函数永久性陷入阻塞。
在kubernetes项目的多个组件中均有使用select阻塞main函数的案例,比如apiserver中的webhook测试组件:
func main(){
server := webhooktesting.NewTestServer(nil)
server.StartTLS()
fmt.Println("serving on",server.URL)
select()
}
以上代码的select语句中不包含case语句和default语句,那么协程(main)将陷入永久性阻塞。
有时我们会使用管道来传输错误,此时就可以使用select语句快速检查管道中是否有错误,避免陷入循环。比如kubernetes调度器中就有类似的用法:
errCh := make(chan error,active)
jm.deelteJobPods(&job,activePods,errÇh) //传入管道用于记录错误
select{
case manageJobErr = <-errCh : //检查是否有错误发生
if manageJobErr != nil {
break
}
defalut: //没有错误,快速结束检查
}
上面的select仅用于尝试从管道中读取错误信息,如果没有错误,则不会陷入阻塞。
有时我们会使用管道来管理函数的上下文,此时可以使用select来创建只有一定时效的管道。比如kubernetes控制器中就有类似的用法:
func waitForStopOrTimeout(stopCh <-chan struct{} , timeout time.Duration) <-chan struct{} {
stopChWithTimeout := make(chan struct{})
go func(){
select{
case <-stopCh: //自然结束
case <-time.After(timeout): //最长等待时间
}
close(stopChWithTimeout)
}()
return stopChWithTimeout
}
该函数返回一个管道,可用于在函数之间传递,但该管道会在指定时间后自动关闭。
研究selec的实现原理,可以帮助我们更清晰的了解以下问题:
select的case语句对应runtime包中的scase(select-case)数据结构:
// Select case descriptor.
// Known to compiler.
// Changes here must also be made in src/cmd/internal/gc/select.go's scasetype.
type scase struct {
c *hchan // chan
kind uint16 //case类型,值有 caseNil caseRecv caseSend caseDefalut,1.16版本后此变量没有了,
elem unsafe.Pointer // data element
}
scase中的成员c表示case语句操作的管道,**由于每个case中仅能存放一个管道,这就直接决定了每个case语句中仅能处理一个管道。**另外编译器在处理case语句时,如果case语句中没有管道操作(不能处理成scase对象),则会给出编译错误:
select case must be receive , send or assign recv
scase中的成员kind表示case语句的类型,每个类型均表示一类管道操作或特殊case(go 1.16版本该成员字段已经取消掉了)。
const (
caseNil = iota //管道的值为nil
caseRecv //读管道的case
caseSend //写管道的case
caseDefault //default
)
类型为caseNil的case语句表示其操作的管道值为nil,由于nil管道既不可读也不可写,意味着这类case永远不会命中,在运行时会被忽略,这正是为什么在case语句中向值为nil的管道中写入数据不会触发panic的原因。
类型为caseRecv的case语句,表示其将从管道中读取数据
类型为caseSend的case语句,表示其将写入数据到管道中
default为特殊类型的case语句,其不会操作管道。另外,每个select语句中仅可存在一个default语句,并且default语句可以出现在任意位置。
scase中的成员elem表示数据存放的地址,根据case类型而具有不同的含义
Go在runtime包中提供了selectgo()函数用于处理select语句:
// selectgo implements the select statement.
// cas0 points to an array of type [ncases]scase, and order0 points to
// an array of type [2*ncases]uint16 where ncases must be <= 65536.
// Both reside on the goroutine's stack (regardless of any escaping in
// selectgo).
// For race detector builds, pc0 points to an array of type
// [ncases]uintptr (also on the stack); for other builds, it's set to
// nil.
// selectgo returns the index of the chosen scase, which matches the
// ordinal position of its respective select{recv,send,default} call.
// Also, if the chosen scase was a receive operation, it reports whether
// a value was received.
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {}
selectgo()函数会从一组case语句中挑选一个case,并返回命中的case下标,对于类型为caseRecv的case,还会返回是否成功的从管道中读取了数据(第二个返回值对于其他类型的case无意义)。
selectgo()函数的实现包括一下几个要点:
最后,使用select读取管道时,尽量检查读取是否成功,以便及时发现管道异常。