判断语句
Go 语言提供了以下几种条件判断语句:
语句 | 描述 |
---|---|
if 语句 | if 语句 由一个布尔表达式后紧跟一个或多个语句组成。 |
if...else 语句 | if 语句 后可以使用可选的 else 语句, else 语句中的表达式在布尔表达式为 false 时执行。 |
if 嵌套语句 | 你可以在 if 或 else if 语句中嵌入一个或多个 if 或 else if 语句。 |
switch 语句 | switch 语句用于基于不同条件执行不同动作。 |
select 语句 | select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。 |
注意:Go 没有三目运算符,所以不支持 ?: 形式的条件判断。
本文只介绍select 语句
基本 select 语法
select 是 Go 中的一个控制结构,类似于 switch 语句。
select 语句只能用于通道操作,每个 case 必须是一个通道操作,要么是发送要么是接收。
select 语句会监听所有指定的通道上的操作,一旦其中一个通道准备好就会执行相应的代码块。
如果多个通道都准备好,那么 select 语句会随机选择一个通道执行。如果所有通道都没有准备好,那么执行 default 块中的代码。
Go 编程语言中 select 语句的语法如下:
go
select {
case <- channel1:
// 执行的代码
case value := <- channel2:
// 执行的代码
case channel3 <- value:
// 执行的代码
// 你可以定义任意数量的 case
default:
// 所有通道都没有准备好,执行的代码
}
以下描述了 select 语句的语法:
-
每个 case 都必须是一个通道
-
所有 channel 表达式都会被求值
-
所有被发送的表达式都会被求值
-
如果任意某个通道可以进行,它就执行,其他被忽略。
-
如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行。
否则:
- 如果有 default 子句,则执行该语句。
- 如果没有 default 子句,select 将阻塞,直到某个通道可以运行;Go 不会重新对 channel 或值进行求值。
select 语句应用演示1
go
package main
import (
"fmt"
"time"
)
func main() {
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++ {
select {
case msg1 := <-c1:
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
}
}
以上代码执行结果为:
received one
received two
以上实例中,我们创建了两个通道 c1 和 c2。
select 语句等待两个通道的数据。如果接收到 c1 的数据,就会打印 "received one";如果接收到 c2 的数据,就会打印 "received two"。
select 语句应用演示2
以下实例中,我们定义了两个通道,并启动了两个协程(Goroutine)从这两个通道中获取数据。在 main 函数中,我们使用 select 语句在这两个通道中进行非阻塞的选择,如果两个通道都没有可用的数据,就执行 default 子句中的语句。
以下实例执行后会不断地从两个通道中获取到的数据,当两个通道都没有可用的数据时,会输出 "no message received"。
go
package main
import "fmt"
func main() {
// 定义两个通道
ch1 := make(chan string)
ch2 := make(chan string)
// 启动两个 goroutine,分别从两个通道中获取数据
go func() {
for {
ch1 <- "from 1"
}
}()
go func() {
for {
ch2 <- "from 2"
}
}()
// 使用 select 语句非阻塞地从两个通道中获取数据
for {
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
default:
// 如果两个通道都没有可用的数据,则执行这里的语句
fmt.Println("no message received")
}
}
}
select 语句的使用场景
以下是一些 select
语句的使用场景:
-
等待多个通道的消息(多路复用)
当我们需要等待多个通道的消息时,使用
select
语句可以非常方便地等待这些通道中的任意一个通道有消息到达,从而避免了使用多个goroutine进行同步和等待。 -
超时等待通道消息
当我们需要在一段时间内等待某个通道有消息到达时,使用
select
语句可以与time
包结合使用实现定时等待。 -
在通道上进行非阻塞读写
在使用通道进行读写时,如果通道没有数据,读操作或写操作将会阻塞。但是使用
select
语句结合default
分支可以实现非阻塞读写,从而避免了死锁或死循环等问题。
因此,select
的主要作用是在处理多个通道时提供了一种高效且易于使用的机制,简化了多个 goroutine
的同步和等待,使程序更加可读、高效和可靠。
channel 与 select 结合的常见用途
利用 default 分支避免阻塞
select 的 default 分支语义:当所有 case 语句里读/写 channel 阻塞时,会执行 default!
无论 channel 是否有 buffer。
有些时候,我们可能不希望阻塞在写入 channel 上,那可以利用 select default 的特性,这样封装一个函数,当写入阻塞时,返回一个 false,让外界可以处理阻塞的情况:
go
func tryWriteChannel(c chan<- int, value int) bool {
select {
case c <- value
return true
default: //其他没就绪时,会执行
return false
}
}
这样使用:
go
//active <- 1 //之前直接写 channel,如果满了,就会阻塞
writed := tryWriteChannel(active, 1) //改成这样,可以在阻塞时,处理相关逻辑
if !writed {
log.Println("failed to write channel")
return
}
实现超时
假如我们想在一个 channel 的读/写操作上加一个超时逻辑,可以通过这样实现: 在 select 代码块中,加一个 case,这个 case 会在超时后执行,这样会结束其他 case。
比如这样:
go
func tryGetSemaphore(c chan<- struct{}) bool {
select {
case c <- struct {}{}:
return true
case <- time.After(1 * time.Second):
//在写 channel 的基础上,额外加一个情况,超时情况
log.Println("timeout!!!")
//1s 后返回,可以在这里做超时处理
return true
}
}
及时调用 timer 的 Stop 方法回收 Timer 资源。
心跳机制
循环执行一个额外的 case,这个 case 会定时返回。
go
func worker() {
heartbeat := time.NewTicker(30 * time.Second)
defer heartbeat.Stop()
for {
select {
case <-c:
// ... do some stuff
case <- heartbeat.C:
//... do heartbeat stuff
}
}
}
time.NewTicker
会创建一个定时执行的心跳,可以把这个 ticker channel 读取的操作放到一个 case 里,这样 select 代码块就会定时执行一次。