select 是 GO 语言中用来提供 IO 复用的机制,它可以检测多个 chan 是否 ready(可读/可写)。

老规矩,我们先来答几道题试试水。

答题环节
  1. 下面程序输出什么?
package main

import (
	"fmt"
	"time"
)

func main() {
	chan1 := make(chan int)
	chan2 := make(chan int)
	
	go func() {
		chan1 <- 1
		time.Sleep(5 * time.Second)
	}()
	go func() {
		chan2 <- 1
		time.Sleep(5 * time.Second)
	}()
	
	select {
		case <- chan1:
			fmt.Println("chan1")
		case <- chan2:
			fmt.Println("chan2")
		default:
			fmt.Println("default")
	}
	fmt.Println("main exit")
}
复制代码

答案:
select 中的 case 执行顺序是随机的,如果某个 case 中的 channel 已经 ready,那么就会执行相应的语句并退 出 select 流程,如果所有 case 中的 channel 都未 ready,那么就会执行 default 中的语句然后退出 select 流程。

由于启动的协程和 select 语句并不能保证执行的顺序,所以也有可能 select 执行时协程还未向channel中写入数据,所以 select 直接执行 default 语句并退出。因此,次程序有可能产生三种输出:

chan1
main exit
复制代码
chan2
main exit
复制代码
default
main exit
复制代码
  1. 下面程序输出什么?
package main

import (
	"fmt"
	"time"
)

func main() {
	chan1 := make(chan int)
	chan2 := make(chan int)
	
	writeFlag := false
	go func() {
		for {
			if writeFlag {
				chan1 <- 1
			}
			time.Sleep(5 * time.Second)
		}
	}()
	go func() {
		for {
			if writeFlag {
				chan2 <- 1
			}
			time.Sleep(5 * time.Second)
		}
	}()
	
	select {
		case <- chan1:
			fmt.Println("chan1")
		case <- chan2:
			fmt.Println("chan2")
	}
	fmt.Println("main exit.")
}
复制代码

答案:
和第一题一样,select 会随机检测各 case 语句中 channel是否 ready,如果有 case 中 channel 已经 ready 则执行相应的 case 语句后退出 select 流程,如果所有的 channel 都未 ready 且没有 default 的话,则会阻 塞等待各个 channel。因此上述程序会一直阻塞。

  1. 下面程序输出什么?
package main

import (
	"fmt"
)

func main() {
	chan1 := make(chan int)
	chan2 := make(chan int)
	
	go func() {
		close(chan1)
	}()
	go func() {
		close(chan2)
	}()
	
	select {
		case <- chan1:
			fmt.Println("chan1")
		case <- chan2:
			fmt.Println("chan2")
	}
	fmt.Println("main exit.")
}
复制代码

答案:
select 会随机检测各 case 语句中 channel 是否 ready,注意已关闭的 channel 也是可读的,所以上述程序中select 不会阻塞,具体执行哪个 case 语句具是随机的。

  1. 下面程序输出什么?
package main

func main() {
  select {
  }
}
复制代码

答案: 对于空的 select 语句,程序会被阻塞,确切的说是当前协程被阻塞,同时 Go 自带死锁检测机制,当发现当前协程再也没有机会被唤醒时,则会发生 panic。所以上述程序会 panic。

实现原理

Go 实现 select 时,定义了一个数据结构表示每个 case 语句(包含defaut),select 执行过程可以类比成一个函数,函数输入 case 数组,输出选中的 case,然后程序流程转到选中的 case块。

源码包 src/runtime/select.go 定义了表示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
	elem unsafe.Pointer // data element
}
复制代码
  • c : 表示当前 case 语句所操作的 channel 指针
  • elem :表示缓冲区地址
归纳总结
  • select 语句中除 default 外,每个 case 操作一个channel,要么读要么写
  • select语句中除 default 外,各 case 执行顺序是随机的
  • select 语句中如果没有 default 语句,则会阻塞等待任一 case
  • select 语句中读操作要判断是否成功读取,关闭的 channel 也可以读取