【Golang】关键字 - select

Posted by 西维蜀黍 on 2021-10-03, Last Modified on 2023-08-23

Overall

The select statement provides another way to handle multiple channels.

It’s like a switch, but each case is a communication:

  • All channels are evaluated.
  • Selection blocks until one communication can proceed, which then does.
  • If multiple can proceed, select chooses pseudo-randomly.
  • A default clause, if present, executes immediately if no channel is ready.
    select {
    case v1 := <-c1:
        fmt.Printf("received %v from c1\n", v1)
    case v2 := <-c2:
        fmt.Printf("received %v from c2\n", v1)
    case c3 <- 23:
        fmt.Printf("sent %v to c3\n", 23)
    default:
        fmt.Printf("no one was ready to communicate\n")
    }

select

Go’s select lets you wait on multiple channel operations. Combining goroutines and channels with select is a powerful feature of Go.

A select blocks until one of its cases can run, then it executes that case. It chooses one at random if multiple are ready.

Example 1

package main

import (
    "fmt"
    "time"
)

func main() {
    // Each channel will receive a value after some amount of time, to simulate e.g. blocking RPC operations executing in concurrent goroutines.
    c1 := make(chan string)
    c2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        c1 <- "one"
    }()
    go func() {
        time.Sleep(2 * time.Second)
        c2 <- "two"
    }()

    for i := 0; i < 2; i++ {
        // We’ll use select to await both of these values simultaneously, printing each one as it arrives.
        select {
        case msg1 := <-c1:
            fmt.Println("received", msg1)
        case msg2 := <-c2:
            fmt.Println("received", msg2)
        }
    }
}
# Note that the total execution time is only ~2 seconds since both the 1 and 2 second Sleeps execute concurrently.
$ time go run select.go 
received one
received two

real    0m2.245s

Example 2

select 语句选择一组可能的 send 操作和 receive 操作去处理。它类似 switch, 但是只是用来处理通讯 (communication) 操作。

它的 case 可以是 send 语句,也可以是 receive 语句,亦或者 default

receive 语句可以将值赋值给一个或者两个变量。它必须是一个 receive 操作。

最多允许有一个 default case, 它可以放在 case 列表的任何位置,尽管我们大部分会将它放在最后。

package main

import "fmt"

func fibonacci(c, quit chan int) {
	x, y := 0, 1
	for {
		select {
		case c <- x:
			x, y = y, x+y
		case <-quit:
			fmt.Println("quit")
			return
		}
	}
}
func main() {
	c := make(chan int)
	quit := make(chan int)
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)
		}
		quit <- 0
	}()
	fibonacci(c, quit)
}

output:

0
1
1
2
3
5
8
13
21
34
quit

如果有同时多个 case 去处理,比如同时有多个 channel 可以接收数据,那么 Go 会伪随机的选择一个 case 处理 (pseudo-random)。如果没有 case 需要处理,则会选择 default 去处理,如果 default case 存在的情况下。如果没有 default case,则 select 语句会阻塞,直到某个 case 需要处理。

需要注意的是,nil channel 上的操作会一直被阻塞,如果没有 default case, 只有 nil channel 的 select 会一直被阻塞。

select 语句和 switch 语句一样,它不是循环,它只会选择一个 case 来处理,如果想一直处理 channel,你可以在外面加一个无限的 for 循环:

for {
	select {
	case c <- x:
		x, y = y, x+y
	case <-quit:
		fmt.Println("quit")
		return
	}
}

Example 3 - Deafault Selection

The default case in a select is run if no other case is ready.

Use a default case to try a send or receive without blocking:

package main

import (
	"fmt"
	"time"
)

func main() {
	tick := time.Tick(100 * time.Millisecond)
	boom := time.After(500 * time.Millisecond)
	fmt.Println("start: ", time.Now())
	for {
		select {
		case <-tick:
			fmt.Println("tick.")
		case <-boom:
			fmt.Println("BOOM!")
			return
		default:
			fmt.Println("    .", time.Now())
			time.Sleep(50 * time.Millisecond)
		}
	}
}

Ouput:

start:  2021-05-30 00:06:25.112943 +0800 +08 m=+0.000076131
    . 2021-05-30 00:06:25.11308 +0800 +08 m=+0.000213931
    . 2021-05-30 00:06:25.163775 +0800 +08 m=+0.050907950
tick.
    . 2021-05-30 00:06:25.214493 +0800 +08 m=+0.101626169
    . 2021-05-30 00:06:25.264906 +0800 +08 m=+0.152038849
tick.
    . 2021-05-30 00:06:25.315246 +0800 +08 m=+0.202378358
    . 2021-05-30 00:06:25.365353 +0800 +08 m=+0.252484648
tick.
    . 2021-05-30 00:06:25.415521 +0800 +08 m=+0.302652447
    . 2021-05-30 00:06:25.46862 +0800 +08 m=+0.355750735
tick.
    . 2021-05-30 00:06:25.518711 +0800 +08 m=+0.405841375
    . 2021-05-30 00:06:25.569271 +0800 +08 m=+0.456400844
tick.
BOOM!

Example 4 - Timeout

select 有很重要的一个应用就是超时处理。 因为上面我们提到,如果没有 case 需要处理,select 语句就会一直阻塞着。这时候我们可能就需要一个超时操作,用来处理超时的情况。

下面这个例子我们会在 2 秒后往 channel c1 中发送一个数据,但是 select 设置为 1 秒超时,因此我们会打印出 timeout 1, 而不是 result 1

import "time"
import "fmt"

func main() {
    c1 := make(chan string, 1)
    go func() {
        time.Sleep(time.Second * 2)
        c1 <- "result 1"
    }()

    select {
    case res := <-c1:
        fmt.Println(res)
    case <-time.After(time.Second * 1):
        fmt.Println("timeout 1")
    }
}

其实它利用的是 time.After 方法,它返回一个类型为 <-chan Time 的单向的 channel,在指定的时间发送一个当前时间给返回的 channel 中。

Example 5 - Timers

timer 是一个定时器,代表未来的一个单一事件,你可以告诉 timer 你要等待多长时间,它提供一个 Channel,在将来的那个时间那个 Channel 提供了一个时间值。下面的例子中第二行会阻塞 2 秒钟左右的时间,直到时间到了才会继续执行。

timer1 := time.NewTimer(time.Second * 2)
<-timer1.C
fmt.Println("Timer 1 expired")

当然如果你只是想单纯的等待的话,可以使用 time.Sleep 来实现。

你还可以使用 timer.Stop 来停止计时器。

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Printf("start, current time: %v\n", time.Now())
	timer2 := time.NewTimer(1 * time.Second)
	go func() {
		<-timer2.C // after 1s, channel received here
		fmt.Printf("Timer 2 expired, current time: %v\n", time.Now())
	}()
	time.Sleep(5 * time.Second)

	fmt.Printf("check Timer 2 has stopped or not, current time: %v\n", time.Now())
	notStop := timer2.Stop() // return false if the timer has already expired or been stopped.
	if notStop {
		fmt.Println("Timer 2 stopped")
	}
}

如果注释掉 time.Sleep(5 * time.Second),output:

start, current time: 2021-05-25 23:28:06.155431 +0800 +08 m=+0.000060591
check Timer 2 has stopped or not, current time: 2021-05-25 23:28:06.155587 +0800 +08 m=+0.000216171
Timer 2 stopped

如果不注释掉 time.Sleep(5 * time.Second),output:

start, current time: 2021-05-25 23:31:36.563422 +0800 +08 m=+0.000061261
Timer 2 expired, current time: 2021-05-25 23:31:37.566743 +0800 +08 m=+1.003413541
check Timer 2 has stopped or not, current time: 2021-05-25 23:31:41.564029 +0800 +08 m=+5.000824104

Example 6 - Ticker

ticker 是一个定时触发的计时器,它会以一个间隔 (interval) 往 Channel 发送一个事件 (当前时间),而 Channel 的接收者可以以固定的时间间隔从 Channel 中读取事件。下面的例子中 ticker 每 500 毫秒触发一次,你可以观察输出的时间。

package main

import (
	"fmt"
	"time"
)

func main() {
	ticker := time.NewTicker(time.Millisecond * 500)
	go func() {
		for t := range ticker.C {
			fmt.Println("Tick at", t)
		}
	}()

	time.Sleep(2 * time.Second)
}

类似 timer, ticker 也可以通过 Stop 方法来停止。一旦它停止,接收者不再会从 channel 中接收数据了。

Output:

Tick at 2021-05-25 23:32:53.587043 +0800 +08 m=+0.500905900
Tick at 2021-05-25 23:32:54.088342 +0800 +08 m=+1.002220770
Tick at 2021-05-25 23:32:54.589596 +0800 +08 m=+1.503489999

Reference