select语句是一种仅能用于通道发送和接收操作的专用语句。
组成和编写
-
一条select语句执行时,会选择其中的某一个分支并执行。
在表现形式上,select 语句与 switch 语句非常类似,但是它们选择分支的方法完全不同。 -
在 select 语句中,每个分支以关键字case开始。但与 switch 语句不同,跟在每个case后面的只能是针对某个通道的发送语句或接收语句。
-
另外,在select关键字的右边并没 有像 switch 语句那样的 switch 表达式,而是直接后跟左花括号。这也与它选择分支的方法有关。
-
无论default case被放置在哪里,分支选择规则都不会改变。
分支选择
在开始执行 select语句的时候,所有跟在case关键字右边的发送语句或接收语句中的通道表达式和元素表达式都会先求值(求值的顺序是从左到右、自上而下的),
无论它们所在的 case是否有可能被选择都会是这样。
package main
import "fmt"
var intChan1 chan int
var intChan2 chan int
var channels = []chan int{intChan1, intChan2}
var numbers = []int{1, 2, 3, 4, 5}
func main() {
select {
// 上-左 上- 右
case getChan(0) <- getNumber(0):
fmt.Println("The 1th case is selected." )
// 下-左 下-右
case getChan(1) <- getNumber(1):
fmt.Println("The 2nd case is selected.")
default:
fmt.Println("Default!")
}
}
func getNumber(i int) int {
fmt.Printf("numbers[%d]\n", i)
return numbers[i]
}
func getChan(i int) chan int {
fmt.Printf("channels[%d]\n", i)
return channels[i]
}
// 输出
channels[0]
numbers[0]
channels[1]
numbers[1]
Default!
前4行输出表明了select语句中跟在case语句后面的那4个表达式的求值顺序。
注 意,通道 intChan1和 intChan2 都未被初始化,向它们发送元素值的操作会被永久阻塞, 这也是有第 5 行输出的缘由。
select 语句被执行时选择了 default case,因为其他两个 case 走不通。在执行 select 语句的时候,运行时系统会自上而下地判断每个 case中的发送或接收操作是否可以立即进行。
“立即进行”:当前goroutine不会因此操作而被阻塞。
这个判断还需要依据通道的具体特性(缓冲或非缓冲)以及那一时刻的具体情况来进行。只要发现有一个 case 上的判断是肯定的,该 case就会被选中。
一当有一个 case 被选中时,运行时系统就会执行该 case 及其包含的语句,而其他case会被忽略。
如果同时有多个case满足条件,那么运行时系统会通过一个伪随机的算法选中一个 case。
另一方面,如果select语句中的所有case都不满足选择条件,并且没有default case,那么当前goroutine就会一直被阻塞于此,
直到至少有一个case中的发送或接收操作可以立即进行为止。因此,你的程序中永远不要出现像下面这样的代码:
如果主goroutine执行了以下代码,那么就会发生死锁!
select {
case getChan(0) <- getNumber(0):
fmt.Println("The 1th case is selected.")
case getChan(1) <- getNumber(1):
fmt.Println("The 2nd case is selected.")
}
-
接收操作符<-可以从一个通道接收一个元素值,
也可以通过与 = 或 := 联接把操作结果赋给一个或两个变量。 -
如果同时对两个变量赋值,那么第二个变量便会指明当前通道是否已被关闭且已无元素值。case中的接收语句当然也支持这种方式。请看下面的代码:
break 语句作用是立即结束当前select语句的执行
var strChan = make(cha string,10)
select {
case e,ok := <-strChan:
if !ok {
fmt.Println("End.")
break
}
fmt.Printf("Received:%v\n",e)
}
与for语句的连用
select语句常常与for语句联用,以便持续操作其中的通道。
package main
import "fmt"
func main() {
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
close(intChan)
syncChan := make(chan struct{}, 1)
go func() {
Loop:
for {
select {
case e, ok := <-intChan:
if !ok {
fmt.Println("End. (break Loop)")
break Loop
}
fmt.Printf("Received: %v\n", e)
}
}
syncChan <- struct{}{}
}()
<-syncChan
}
// 输出
Received: 0
Received: 1
Received: 2
Received: 3
Received: 4
Received: 5
Received: 6
Received: 7
Received: 8
Received: 9
End. (break Loop)
- 请注意其中的语句 break Loop。这是一条带标签的 break 语句,
- Loop为标签的名字,意为中断紧贴于该标签之下的那条语句的执行。
上述代码中的 Loop:指明了其下方的for语句就是那条语句。只有如此,break 语句才能够正确地结束外层 for 循环的执行。 - 否则,如果该语句不带标签,那么就只能结束其所在的 select 语句的执行,而那个for 循环就会一直执行下去,永远不会结束。
- 顺便说一句, break Loop 和Loop:必须是遥相呼应的,前者必须包含在后者下方紧邻的那条语句中。
- 通过上面这一系列的示例和讲解,你已经对select语句的编写方法和执行方式都有了一定的理解。注意,这其中的(以及之前的)很多规则都是只针对缓冲通道的。与非缓冲通道相关的各种使用方法和技巧,马上就会揭晓。