Go 语言中 select 的用法
谈到select大家可能会想到swicth,相信大家对 switch 并不陌生,select 和 switch 的共同点在于都通过 case 来处理,但两者处理的内容完全不同,且互不兼容。
switch 的特性
switch 语句可以处理各种类型和类型断言操作,例如 interface{} 类型判断。以下是 switch 的几个特性:
- 可用于各种数据类型的值判断。
- 支持
interface{}.(type)类型断言。 - 按照
case顺序逐一匹配并执行。
例如
package main
var (
i interface{}
)
func convert(i interface{}) {
switch t := i.(type) {
case int:
println("i is integer", t)
case string:
println("i is string", t)
case float64:
println("i is float64", t)
default:
println("type not found")
}
}
func main() {
i = 100
convert(i)
i = float64(45.55)
convert(i)
i = "foo"
convert(i)
convert(float32(10.0))
}
结果
i is integer 100
i is float64 +4.555000e+001
i is string foo
type not found
select 的特性
在go语言中select 专用于处理 channel,如果不是 channel,会报错。以下是 select 的几个关键点:
default分支会直接执行。- 如果没有
default,select会阻塞。 - 如果未向 channel 发送数据,可能会引发死锁(
panic)。
以下是一些 select 的实际用法和示例:
随机选择 Random Select
当多个 case 中涉及同一个 channel 时,select 会随机选择一个分支执行。
package main
import "fmt"
func main() {
ch := make(chan int, 1)
ch <- 1
select {
case <-ch:
fmt.Println("random 01")
case <-ch:
fmt.Println("random 02")
}
}
结果:
执行时有时输出 random 01,有时输出 random 02。这是因为 select 会在有多个匹配 case 时随机选择执行。
如果没有向 channel 发送数据,程序会因为死锁(deadlock)而崩溃。例如:
func main() {
ch := make(chan int, 1)
select {
case <-ch:
fmt.Println("random 01")
case <-ch:
fmt.Println("random 02")
}
}
如何解决
通过增加 default 分支可以避免死锁:
func main() {
ch := make(chan int, 1)
select {
case <-ch:
fmt.Println("random 01")
case <-ch:
fmt.Println("random 02")
default:
fmt.Println("exit")
}
}
运行后会输出 exit,主程序不会因为没有值读取而阻塞。
超时机制 Timeout
使用 select 读取 channel 时,通常会实现一个超时机制,避免长时间阻塞。例如:
package main
import (
"fmt"
"time"
)
func main() {
timeout := make(chan bool, 1)
go func() {
time.Sleep(2 * time.Second)
timeout <- true
}()
ch := make(chan int)
select {
case <-ch:
case <-timeout:
fmt.Println("timeout 01")
}
}
通过向 timeout channel 发送值来触发超时。也可以使用 time.After 来实现超时:
select {
case <-ch:
case <-timeout:
fmt.Println("timeout 01")
case <-time.After(time.Second * 1):
fmt.Println("timeout 02")
}
time.After 会返回一个 chan time.Time,如果超过指定时间,select 会执行相应分支。
检查 channel 是否已满
通过 select 和 default 结合,可以检查 channel 是否已满:
package main
import (
"fmt"
)
func main() {
ch := make(chan int, 1)
ch <- 1
select {
case ch <- 2:
fmt.Println("channel value is", <-ch)
fmt.Println("channel value is", <-ch)
default:
fmt.Println("channel blocking")
}
}
运行后会输出 channel blocking,因为 ch 的缓冲区大小为 1,已经满了。
将缓冲区改为 2 后,可以继续向 channel 发送值:
func main() {
ch := make(chan int, 2)
ch <- 1
select {
case ch <- 2:
fmt.Println("channel value is", <-ch)
fmt.Println("channel value is", <-ch)
default:
fmt.Println("channel blocking")
}
}
select 与 for 的结合
当需要持续监听多个 channel 时,可以结合 for 和 select 实现。以下是示例代码:
package main
import (
"fmt"
"time"
)
func main() {
i := 0
ch := make(chan string, 0)
defer func() {
close(ch)
}()
go func() {
LOOP:
for {
time.Sleep(1 * time.Second)
fmt.Println(time.Now().Unix())
i++
select {
case m := <-ch:
println(m)
break LOOP
default:
}
}
}()
time.Sleep(time.Second * 4)
ch <- "stop"
}
结果
1574474619
1574474620
1574474621
1574474622
程序会每秒打印当前时间戳,直到主线程向 ch 发送 "stop"。
需要注意
-
没有
default的情况下,select会阻塞直到某个 channel 有数据。 -
带
default的select,如果没有满足的 case,default会立即执行。 -
在
for + select的场景下,如果需要跳出循环,可以使用带标签的break,比如:LOOP: for { select { case m := <-ch: println(m) break LOOP } }