01
介绍
Go 协程之间通过 channel
通信,但是 channel
读写取决于自身特性,即是否有可写入缓冲区、缓冲区中是否有数据、是否已关闭...
为了检测 channel
的特性,Go 提供了一个关键字 select
,可用于实现 I/O
多路复用机制。
本文我们介绍 Go 关键字 select
的使用方式。
02
使用方式
Go 关键字 select
中包含 case
语句和 default
语句,其中 default
语句可以认为是一种特殊的 case
语句。
因为 default
语句不负责处理 channel
的读写,它可以在 select
中的任意位置,且仅能包含一个 default
语句。在所有 case
语句都不满足执行条件时,default
语句将被执行(建议尽量不要省略 default
语句)。
我们通过代码片段,分别介绍 select
在检测到 channel
不同特性时,得到的运行结果。
空 select
接下来,我们阅读一段代码。
代码语言:javascript复制func main() {
fmt.Println("Golang 语言开发栈")
go func() {
fmt.Println("**** 公众号")
}()
}
阅读上面这段代码,读者朋友们认为 Go 协程中的打印语句可以正常输出吗?
读者朋友们如果运行代码,会发现 Go 协程中的打印语句还没有执行,程序就已经退出了,这是因为 main
函数中的打印语句已经执行完成,所以会退出程序。
如果我们希望 Go 协程中的打印语句也执行,可以在 main
函数中使用 select{}
将 main
阻塞,Go 协程中的打印语句就有机会执行了。但是,这会导致死锁(可以根据实际应用场景选择是否使用)。
无缓冲 channel
接下来,我们再读一段可以导致死锁的代码:
代码语言:javascript复制func main() {
c := make(chan string)
DoChannel(c)
}
func DoChannel(c chan string) {
var receive string
send := "golang"
select {
case receive = <-c:
fmt.Println(receive)
case c <- send:
fmt.Println(send)
}
}
阅读上面这段代码,我们定义一个函数 DoChannel()
,该函数接收的参数是一个 string
类型的 channel
,函数体中使用 select
中的两个 case
语句,分别对参数进行接收和发送操作。
运行代码,select
阻塞。
因为,我们传参的 c
是无缓冲 channel
,所以它即不能读也不能写,两个 case
语句都不执行,select
陷入阻塞,导致死锁(此处为了行文,故意没有 default
语句)。
无数据,有缓冲channel
我们将上面这段代码,稍微修改一下,将入参的 c
改为 1 个缓冲区大小的 channel
(未写入数据)。代码如下:
func main() {
c := make(chan string, 1)
DoChannel(c)
}
运行代码,写执行,读未执行。
即 select
中的对入参 channel
进行发送操作的 case
语句被执行,因为入参 c
是一个有 1 个缓冲区大小的 channel
,并且该 channel
中还没有数据,所以读取操作的 case
语句没有读取到数据,不满足执行条件。
有缓冲区,已写满数据 channel
我们再修改一下入参 c
,将入参的 c
改为 1 个缓冲区大小的 channel
,并且写入字符串 Go
。代码如下:
func main() {
c := make(chan string, 1)
c <- "Go"
DoChannel(c)
}
运行代码,读执行,写未执行。
即 select
中的对入参 channel
进行接收操作的 case
语句被执行,因为入参 c
是一个有 1 个缓冲区大小,并且已写满数据,所以读取操作的 case
语句可以读取到数据,满足执行条件。
而写入操作的 case
无法写入数据,不满足执行条件。
有缓冲区,有数据,可写数据 channel
最后一种场景是既能读取也能写入的 channel
,我们修改一下入参 c
,将入参 c
改为 2 个缓冲区大小的 channel
,其中 1 个缓冲区写入字符串 Go
,另外 1 个缓冲区还可以写入数据。代码如下:
func main() {
c := make(chan string, 2)
c <- "Go"
DoChannel(c)
}
通过多次运行代码,会发现读取和写入的 case
语句都有机会执行,因为两个 case
语句都满足执行条件,但是只能有 1 个 case
语句执行,select
会随机执行其中 1 个 case
语句。
至此,我们已经介绍了 5 种 channel
在 select
中的运行结果。
case 语句中声明变量
上面的代码中,我们发现在两个 case
语句中,读操作我们将读取到的数据赋值给变量 receive
,实际上,我们也可以省略变量赋值操作。
如果我们需要将读取到的数据,赋值给变量的话,一般建议将读取 channel
返回的两个值全部接收,其中一个是读取到的数据,另外一个是布尔值,代表 channel
中没有数据,并且已被关闭。代码如下:
func main() {
c := make(chan string)
close(c)
DoChannelV2(c)
}
func DoChannelV2(c chan string) {
var (
receive string
ok bool
)
select {
case receive, ok = <-c:
if !ok {
fmt.Println("no data")
} else {
fmt.Println(receive)
}
}
}
阅读上面这段代码,我们使用 close
将 c
关闭。select
中的读操作 case
语句,可以通过 ok
的值,得到 channel
中没有数据,且已被关闭,不必打印空数据。
03
总结
本文我们了解到 select
中的 case
语句可以读取 channel
,多个 case
语句仅能其中 1 个被执行。
每个 case
语句仅能对 1 个 channel
进行读写操作,如果读操作未读取到数据将陷入阻塞,如果写操作无法写入数据将陷入阻塞,如果所有 case
语句中的 channel
都陷入阻塞时,select
也会陷入阻塞。
为了避免 select
陷入阻塞,我们可以使用 default
语句,需要注意的是,default
语句可以在 select
的任意位置,但是仅能包含 1 个,而 case
语句可以包含多个。
推荐阅读
- Go 微服务框架 go-micro 使用客户端 RPC 调用服务端方法返回 408 怎么解决?
- Go 语言怎么解决编译器错误“err is shadowed during return”?
- Go 语言各个版本支持 Go Modules 的演进史
- Go 语言实现创建型设计模式 - 工厂模式
- Golang 语言的编程技巧之类型