go select原理及使用场景
原理
select 控制结构中的 case
type scase struct {
c *hchan // case中使用的channel
elem unsafe.Pointer // 数据元素的指针
}
编译器在中间代码生成期间会根据 select
中 case
的不同对控制语句进行优化,优化逻辑会在
// src/cmd/compile/internal/walk/select.go:31
func walkSelectCases(cases []*ir.CommClause) []ir.Node {
会有下面 4 种情况:
-
select
不存在任何的case
;此时会执行:
func block() { gopark(nil, nil, waitReasonSelectNoCases, traceEvGoStop, 1) // forever }
导致 goroutine 永远阻塞,但传入的
waitReasonSelectNoCases
会使 goroutine 报错并结束程序:go run main.go fatal error: all goroutines are asleep - deadlock! goroutine 1 [select (no cases)]: main.main() /Users/user/go/src/go-demo/select/main.go:4 +0x17 exit status 2
-
select
只存在一个case
; -
select
存在两个case
,其中一个case
是default
;会变成 channel 的非阻塞收发操作:
// 非阻塞收 select { case <-ch: println("case1") default: println("default") } // 非阻塞发 select { case ch <- 0: println("case1") default: println("default") }
-
select
存在多个case
;
案例
-
类似 c 语言的多路复用,可以同时监听多个 channel。当有 channel 可读可写之前会一直阻塞
package main import "time" func main() { ch := make(chan int) go func() { // 每5秒往channel写数据 for range time.Tick(5 * time.Second) { ch <- 0 } }() for { select { case <-ch: // 阻塞等,直到channel有数据 println("case1", time.Now().String()) } } }
执行结果:
go run main.go case1 2022-08-14 22:14:07.59665 +0800 CST m=+5.001220053 case1 2022-08-14 22:14:12.596649 +0800 CST m=+10.001247928 case1 2022-08-14 22:14:17.595772 +0800 CST m=+15.000400037
-
case 中的表达式必须都是 channel 的收发操作
package main import "time" func main() { ch := make(chan int) go func() { for range time.Tick(5 * time.Second) { ch <- 0 } }() for { select { case ch: // 此处会编译不通过 println("case1", time.Now().String()) } } }
执行结果:
go run main.go # command-line-arguments ./main.go:15:3: select case must be receive, send or assign recv // 必须是收发的channel ./main.go:15:8: ch evaluated but not used
-
当
select
中的两个case
同时被触发时,会随机执行其中的一个package main import "time" func main() { ch := make(chan int) go func() { for range time.Tick(time.Second) { ch <- 0 } }() for { select { case <-ch: println("case1") case <-ch: println("case2") } } }
执行结果:
go run main.go case1 case1 case1 case1 case1 case1 case2 case1 case2
两个
case
都是同时满足执行条件的,如果我们按照顺序依次判断,那么后面的条件永远都会得不到执行,而随机的引入就是为了避免饥饿问题的发生 -
如果
select
控制结构中包含default
语句,当存在可以收发的 Channel 时,直接处理该 Channel 对应的case
,当不存在可以收发的 Channel 时,执行default
中的语句package main import "time" func main() { ch := make(chan int) go func() { for range time.Tick(time.Second * 2) { ch <- 0 } }() for { select { case <-ch: println("case1") default: time.Sleep(time.Second) println("default") } } }
执行结果:
go run main.go default default case1 default default case1