什么是select#

select语句用于从多个读写通道中进行选择。select语句将会阻塞,直到有一个读写通道就绪。如果有多个读写通道就绪,会从中任意选择一个。select语法类似switch语句,除了每个case语句必须是通道操作。

让我们深入代码以便更好的理解。

package main

import (
	"fmt"
	"time"
)

func server1(ch chan string) {
   
     
	time.Sleep(6 * time.Second)
	ch <- "from server1"
}
func server2(ch chan string) {
   
     
	time.Sleep(3 * time.Second)
	ch <- "from server2"

}
func main() {
   
     
	output1 := make(chan string)
	output2 := make(chan string)
	go server1(output1)
	go server2(output2)
	select {
   
     
	case s1 := <-output1:
		fmt.Println(s1)
	case s2 := <-output2:
		fmt.Println(s2)
	}
}

在上述程序中,server1函数(第8行)先休眠6秒后往信道output1写入文本。server2函数(第12行)休眠3秒后往信道output2写入文本。

在主函数在第 20 行和第 21 行中分别调用协程server1server2.

程序运行到了第 22select语句,select语句阻塞直到其中一个case语句就绪。在上述程序中,server1协程休眠6s后写入信道output1,而协程server2只休眠3秒就往信道output2写数据。所以select语句将会阻塞3秒等待协程server2往信道output2写数据。3秒后程序打印信息:from server2 之后程序结束。

select应用场景#

上述代码之所以命名函数为server1server2,是为了解释实际使用select的场景。

让我们假设有一个关键业务应用,我们需要尽快给用户返回信息。该应用的数据库是复杂的,而且存储在世界各地不同的服务器上。假设函数server1server2实际上与这些不同的服务器中的两个服务器通信。每个服务器的响应时间取决于每个服务器的负载和网络延迟。我们向两个服务器发送请求,用select语句在相关信道上等待响应。第一个响应的服务器将被select语句所选择,其他响应将被忽略。使用这种方法,我们可以同时向多个服务器发出请求,这样用户就可以得到最快的响应。

defalut case#

在没有case 准备就绪时,可以执行 select 语句中的 default case。这个通常用来防止select语句一直阻塞。

package main

import (
	"fmt"
	"time"
)

func process(ch chan string) {
   
     
	time.Sleep(10500 * time.Millisecond)
	ch <- "process successful"
}

func main() {
   
     
	ch := make(chan string)
	go process(ch)
	for {
   
     
		time.Sleep(1000 * time.Millisecond)
		select {
   
     
		case v := <-ch:
			fmt.Println("received value: ", v)
			return
		default:
			fmt.Println("no value received")
		}
	}

}

上述程序第 8 行的 process 函数休眠了 10500 毫秒(10.5 秒),然后向 ch 信道发送数据 process successful 。在主函数的第 15 行,并发地调用了这个函数。

在并发地调用了 process 协程之后,主协程进入无限for循环,每次循环先休眠1000 毫秒(1 秒),然后执行select语句。在前10500 毫秒中,select的第一个case分支case v:= <-ch:信道不会就绪,因为process协程休眠10.5秒才会向ch信道发送数据。因此default case分支将被执行,程序打印"no value received"10次。

10.5 秒之后,process 协程会在第 10 行向 ch 信道发送数据 process successful。现在,将执行 select 语句的第一个 case,程序会打印 received value: process successful,然后程序终止。该程序会输出:

no value received  
no value received  
no value received  
no value received  
no value received  
no value received  
no value received  
no value received  
no value received  
no value received  
received value:  process successful  

死锁和default case#

package main

func main() {
   
     
	ch := make(chan string)
	select {
   
     
	case <-ch:
	}
}

上面程序的第 4 行,我们创建了一个信道 ch。我们在 select 内部(第6行),试图读取信道 ch。由于没有其他的 Go 协程向该信道写入数据,因此 select 语句会一直阻塞,导致死锁。该程序运行时会触发 panic,报错信息如下:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:  
main.main()  
    /tmp/sandbox416567824/main.go:6 +0x80

如果存在default case,就不会发生死锁,因为在没有其他 case 准备就绪时,会执行default case。我们用default case重写程序如下:

package main

import "fmt"

func main() {
   
     
	ch := make(chan string)
	select {
   
     
	case <-ch:
	default:
		fmt.Println("default case executed")
	}
}

运行程序,输出如下:

default case executed

同样的,在信道为nil的情况下,select语句也会执行default case

package main

import "fmt"

func main() {
   
     
	var ch chan string
	select {
   
     
	case v := <-ch:
		fmt.Println("received value", v)
	default:
		fmt.Println("default case executed")

	}
}

在上面程序中,ch 信道等于 nil,而我们试图在 select 中读取 ch(第8行)。如果没有default caseselect 会一直阻塞,导致死锁。由于我们在 select 内部加入了default case,程序会执行它,并输出:

default case executed

随机选择#

select语句在有多个case分支准备就绪的情况下,会随机选择一个分支执行。

package main

import (
	"fmt"
	"time"
)

func server1(ch chan string) {
   
     
	ch <- "from server1"
}
func server2(ch chan string) {
   
     
	ch <- "from server2"

}
func main() {
   
     
	output1 := make(chan string)
	output2 := make(chan string)
	go server1(output1)
	go server2(output2)
	time.Sleep(1 * time.Second)
	select {
   
     
	case s1 := <-output1:
		fmt.Println(s1)
	case s2 := <-output2:
		fmt.Println(s2)
	}
}

在上面程序第 18 行和第 19 行分别调用了 server1server2 两个协程。然后,主程序休眠了 1 秒钟(第20行)。当程序执行到select 语句时,server1 已经把 from server1 写到了 output1 信道上,而 server2 也同样把 from server2 写到了 output2 信道上。因此这个 select 语句中的两种情况都准备好执行了。如果你运行这个程序很多次的话,输出会是 from server1 或者 from server2,变化取决于随机选择。

空select#

package main

func main() {
   
       
    select {
   
     }
}

你觉的上述程序的会出现什么结果?

我们知道select语句将会被阻塞直到其中一个case分支可执行。这个例子,select语句没有任何case分支,因此它将被永久阻塞导致死锁。程序将会触发 panic,输出如下:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select (no cases)]:  
main.main()  
    /tmp/sandbox299546399/main.go:4 +0x20