Golang基础-Select
基本概念
- select 是 Go 中的一个控制结构,类似于 switch 语句。
- select 语句只能用于通道(channel)操作,每个 case 必须是一个通道操作,要么是发送要么是接收。
- select 语句会监听所有指定的通道上的操作,一旦其中一个通道准备好就会执行相应的代码块。
- 如果多个通道都准备好,那么 select 语句会随机选择一个通道执行。如果所有通道都没有准备好,那么执行 default 块中的代码。
select监听一堆channel,哪个channel来数据了就执行相应的代码。来数据了就是就绪,就是有人往channel里写数据了。
例子
下面的代码模拟火箭发射倒计时程序,倒数10秒没人中断就发射,否则终止。终止的操作是按键盘的回车,用一个goroutine监听是否有这个操作。这个等待回车的goroutine实际上是阻塞了,直到输入回车才停止阻塞,继续执行下面的代码。
通过新建一个名为abort的channel来表示是否有人按下回车终止发射。一旦有人按下回车,上面说的那个等待输入的处于阻塞状态的goroutine就继续执行下一行代码,也就是往abort这个channel里写数据。(注意这里的空结构体作用)abort一旦数据就绪,可以被读取,那么select就有机会选择相应的代码块执行,这部分代码就是终止发射。
注意ticker.C也是一个channel,每隔一秒往里面写一条数据。for循环每隔一秒打印一次倒数数据,为什么是一秒?因为select的两个case都是阻塞的,ticker.C等待一秒变得就绪,abort则是有人输入回车才会就绪。所以最终结果就是,如果一直没人输入回车,这个select只有等ticker等一秒变得就绪才能继续执行。所以每次for循环都会阻塞一秒钟。
ticker最后要stop,否则一直往channel里写数据。goroutine泄露?可以想象成这个ticker也是一个goroutine,如果不stop,那么他一直存活,尽管我们的程序已经不需要用他了,但他还是默默无闻地往channel里写数据。
package main
import (
"fmt"
"os"
"time"
)
func launch() {
fmt.Println("Launching rockets")
}
func main() {
abort := make(chan struct{})
go func() {
os.Stdin.Read(make([]byte, 1))
abort <- struct{}{}
}()
fmt.Println("Commencing countdown. Press return to abort.")
ticker := time.NewTicker(1 * time.Second)
for countdown := 10; countdown > 0; countdown-- {
fmt.Println(countdown)
select {
case <-ticker.C: // receive from the ticker's channel
// Do nothing.
case <-abort:
fmt.Println("Launch aborted!")
return
}
}
ticker.Stop() // cause the ticker's goroutine to terminate
launch()
}
下面是另一个例子。这个例子演示了生产者消费者模式。通过done channel来通知select结束监听。这里是直接close(done),因为读一个已经关闭的channel,会读到0值,如果还写了ok的话,会读到false。其实这么写我觉得不太好,还不如往done里写一条数据容易理解。
done这个channel,如果没有外部干扰,是一直处于非就绪的状态,没有东西可以读。但是,一旦将他关闭,我们就能读到相应的0值。我们这里只关心能不能读,不关系读出来什么,所以用close能实现一样的效果。
func main() {
messages := make(chan int, 10)
done := make(chan bool)
defer close(messages)
// consumer
go func() {
ticker := time.NewTicker(1 * time.Second)
for _ = range ticker.C {
select {
case <-done:
fmt.Println("child process interrupt...")
return
default:
fmt.Printf("send message: %d\n", <-messages)
}
}
}()
// producer
for i := 0; i < 10; i++ {
messages <- i
}
time.Sleep(5 * time.Second)
close(done)
time.Sleep(1 * time.Second)
fmt.Println("main process exit!")
}
Context
将上面的倒数发射程序改为使用context实现。
context作用:
- 上层任务取消后,所有的下层任务都会被取消;
- 中间某一层的任务取消后,只会将当前任务的下层任务取消,而不会影响上层的任务以及同级任务。
package main
import (
"context"
"fmt"
"os"
"time"
)
func launch() {
fmt.Println("Launching rockets")
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go func() {
os.Stdin.Read(make([]byte, 1))
cancel()
}()
fmt.Println("Commencing countdown. Press return to abort.")
ticker := time.NewTicker(1 * time.Second)
for countdown := 10; countdown > 0; countdown-- {
fmt.Println(countdown)
select {
case <-ticker.C: // receive from the ticker's channel
// Do nothing.
case <-ctx.Done():
fmt.Println("Launch aborted!")
return
}
}
ticker.Stop() // cause the ticker's goroutine to terminate
launch()
}
将context想象成一棵树,一停停一串。一个请求超时了,基于该请求的所有操作都停止,防止浪费资源。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)