管道选择器(select)

什么是select

Golang中select用来从多个channel中选择性的获取数据,这里的选择性主要是指的快慢顺序,哪个channel先收到数据,就读取哪个channel中的数据。如果多个channel同时准备好了数据,那就随机选取一个。select语句的语法跟switch语句非常像,只是case语句是对channel的读取语句。下面我们通过一个例子来理解下。

举例

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)
    }
}

这里我们创建了两个不同的channeloutput1以及output2。并创建了两个协程,对应server1server2。其中server1中过了6秒向channel output1中写入了数据,server2中过了2秒向channel output2中写入了数据。

主协程中的select语句会阻塞主协程的执行,直到某个case语句读取到数据,由于server2先向channel中写入数据,因此select语句先读取到server2中写入的数据。该程序输出如下:

from server2

如果了解Node.js的话你会发现,Golang中的select其实跟Node.js中的Promise.race功能差不多。

select真实适用场景

我们在前面一段程序中是特意将两个协程任务命名为server1server2的,就是为了模拟真实应用场景。

假设我们有个应用对性能要求极其苛刻,我们需要尽可能快的返回数据。而数据是备份在不同地区的多个服务器上。我们假设前面的server1server2分别去两个不同的数据库服务器上拉取数据。那么到底哪个数据库服务器先返回数据取决于各个服务器的负载和网络情况。由于我们不知道哪个服务先返回,因此我们同时创建两个服务,通过select,我们可以选择先返回的数据,而后返回的数据则抛掉不用。

默认case

switch语句类型,我们也可以给select语句提供默认case。默认case会在其他case都读取不到数据时执行,默认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")
        }
    }

}

这里我们为process函数创建了一个协程,该协程内暂停了10秒多点时间,然后将字符串"process successful"写入了channel

创建完该协程后,主协程进入了一个无限循环。每隔1秒钟,调用select语句读取channel ch中的数据,或者执行默认case,由于前面10秒内channel ch内都没写入数据,因此会执行默认case,打印10次no value received。但是第10秒到第11秒的那次循环就从channel ch内读到了数据,接着打印并退出主协程。该程序输出如下:

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

死锁

package main

func main() {
    ch := make(chan string)
    select {
    case <-ch:
    }
    ch <- "Hello" // 注意即便有这行代码,也是死锁
}

这里我们创建了一个channel ch。我们在select语句中尝试从ch中读数据。但是由于没有其他goroutine像该channel中写入数据,因此会陷入死锁状态。这里需要注意,虽然后面有写入数据的语句ch <- "Hello",但是由于select语句已经阻塞了,因此已经是死锁状态了,后面的语句根本没机会执行,必须是其他协程向ch中写入数据才行。该程序会报如下运行时错误:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
        /Users/goWorkspace/src/test/main.go:6 +0x57
exit status 2

当然如果我们提供了默认case语句,那么就不会陷入死锁状态了。比如如下程序:

package main

import "fmt"

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

该程序输出为:default case executed

这里再补充一点,即使创建的ch未手动初始化,即为nil,只要提供了默认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")
    }
}

这里什么的channel chnil。这里selectcase语句v := <-ch,永远读取不到数据,相当于陷入了死锁状态。但是由于提供了默认case语句,因此也不会阻塞。该程序输出:default case executed

随机选择

select语句中多个case都ready时,会随机选取一个。

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)
    }
}

这里我们还是创建了两个协程server1server2,在这两个协程内部都是立即向channel中写入了数据。主协程暂停了1秒钟,然后执行select语句。执行select语句时由于两个channel的数据均以准备好,因此两个case语句均判断通过,这时随机选取一个case分支进行执行。我们本机运行会看到,该程序多次允许的输出结果会在两个case中随机出现。

当心空select语句

如果select语句为空,即不含有一个case语句,也会进入死锁状态。

package main

func main() {
    select {}
}

该程序报如下运行时错误:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select (no cases)]:
main.main()
        /Users/goWorkspace/src/test/main.go:4 +0x20
exit status 2

Last updated