概述
这里又有多路复用,但是Go中的这个多路复用不同于网络中的多路复用。在Go里,select用于同时等待多个通信操作(即多个channel的发送或接收操作)。Go中的channel可以参考我的文章:逐步学习Go-并发通道chan(channel)
拆字解释
-
多路:指的是多个channel操作路径。你可以在select块中定义多个case,每个case对应一个channel上的I/O操作(发送或接收)。
-
复用:指的是select的功能,它可以监听多个channel上的事件,并且仅当其中一个channel准备就绪时才会执行相关操作。这样,单个goroutine可以高效地等待多个并发事件而不是单个事件。
复用的是goroutine,一个goroutine使用select可以监听多个信道。
整体来讲:Select就是为channel设计的。
select语法
Go语言中的select关键字功能在概念上与操作系统的select类似,区别在于Go的select是用于goroutine监听多个channel的可读或可写状态。
Go的select允许在channel上进行非阻塞收发,同时当多个channel同时响应时,select会随机执行其中的一个case。
Go的select语句可以包含一个default分支,使得在没有channel准备好时,不会阻塞goroutine,而是执行default分支。
select {
case <-ch:
println("recieved")
case <-time.After(10 * time.Second):
println("Timeout")
default:
printStr = "Hello Select"
}
接下来我们来看场景用例。
在下面的场景测试用例中,我们定义了三个channel: ch1, ch2和ch3。
select只有一个case条件满足
我们创建完成三个channel以后,我们只想ch1发送消息,那么在select三个channel时只有 case <- ch1可以满足,用例执行会输出"Recieved ch1"。
func TestSelect_ShouldRecvChan1_WhenChan1CaseWasFullfilled(t *testing.T) {
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
ch3 := make(chan int, 1)
chans := []chan int{ch1, ch2, ch3}
var wg sync.WaitGroup
wg.Add(1)
go func() {
chans[0] <- 1
wg.Done()
}()
wg.Wait()
select {
case <-ch1:
println("Recieved ch1")
case <-ch2:
println("Recieved ch2")
case <-ch3:
println("Recieved ch3")
case <-time.After(10 * time.Second):
println("Timeout")
default:
}
}
select有多个case条件满足
在这个场景中,我们使用三个goroutine向三个channel都发送了消息,然后等待所有的goroutine执行完成确保3个channel都接收到了消息,那么select会随机选择一个case条件执行(如果自己测试需要多执行几次,因为不止下一次会执行那个case分支)。
如果select中的多个case同时满足,Go语言如何进行选择。Go语言官方文档规定,当多个case都可以运行时,Go会按照"伪随机"的方式来选择一个case执行。这个"伪随机"是指,它不是完全随机的,而是通过一定的算法进行选择,以防止某个channel在高并发的情况下,出现饿死(被忽略)的情况。
func TestSelect_ShouldRandomEnterCaseBranch_WhenAllChannelsCaseWereFullfilled(t *testing.T) {
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
ch3 := make(chan int, 1)
chans := []chan int{ch1, ch2, ch3}
var wg sync.WaitGroup
wg.Add(3)
for i := 0; i < 3; i++ {
go func(i int) {
chans[i] <- 1
wg.Done()
}(i)
}
wg.Wait()
select {
case <-ch1:
println("Recieved ch1")
case <-ch2:
println("Recieved ch2")
case <-ch3:
println("Recieved ch3")
case <-time.After(10 * time.Second):
println("Timeout")
default:
}
}
select没有条件满足-阻塞(Deadlock)
在这个场景,我们只是创建了3个channel,但是没有向三个channel发送消息,那么执行select时go会panic, 错误信息为:Deadlock。
func TestSelect_ShouldBlock_WhenNoCaseWasFullfilled(t *testing.T) {
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
ch3 := make(chan int, 1)
select {
case <-ch1:
println("Recieved ch1")
case <-ch2:
println("Recieved ch2")
case <-ch3:
println("Recieved ch3")
}
}
select没有条件满足-超时
在这个场景中,我们创建了三个channel,然后没有向这三个channel中发送消息,最后我们使用select尝试从三个channel中接收消息,但是我们在case中增加了一个超时检测。
在这个场景中,select会在10秒后执行 case <-time.After(10 * time.Second):
分支因为管道条件没有被满足且没有default
。
func TestSelect_ShouldTimeout_WhenNoCaseWasFullfilled(t *testing.T) {
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
ch3 := make(chan int, 1)
select {
case <-ch1:
println("Recieved ch1")
case <-ch2:
println("Recieved ch2")
case <-ch3:
println("Recieved ch3")
case <-time.After(10 * time.Second):
println("Timeout")
}
}
select没有条件满足-default
在这个场景中,代码和上面的差别在于我们添加了default
分支,添加了default后如果所有case
没有条件满足则执行default分支,所以你执行这个用例控制台会打印Default
。
func TestSelect_ShouldRunDefaultBranch_WhenNoCaseWasFullfilledAndHasDefaultBranch(t *testing.T) {
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
ch3 := make(chan int, 1)
select {
case <-ch1:
println("Recieved ch1")
case <-ch2:
println("Recieved ch2")
case <-ch3:
println("Recieved ch3")
case <-time.After(10 * time.Second):
println("Timeout")
default:
println("Default")
}
}
select关闭的channel接收
在这个场景中,我们创建3个channel然后理解关闭,最后使用select来读取三个channel,那么根据关闭后chnanel的定义:channel
在关闭后永远都可以读取,那么select 的case条件可以被满足且随机选择一个case分支执行,只是读取到的都是“0”值。
注意:0值这个是根据不同类型而不一样的,而且go是严格类型检查,nil是不通用的
func TestSelect_ShouldRecvZeroValue_WhenSelectFromClosedChannel(t *testing.T) {
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
ch3 := make(chan int, 1)
close(ch1)
close(ch2)
close(ch3)
select {
case value := <-ch1:
if value == 0 {
println("Recieved zero value from ch1")
} else {
println("Recieved ch1")
}
case value := <-ch2:
if value == 0 {
println("Recieved zero value from ch2")
} else {
println("Recieved ch2")
}
case value := <-ch3:
if value == 0 {
println("Recieved zero value from ch3")
} else {
println("Recieved ch3")
}
case <-time.After(10 * time.Second):
println("Timeout")
default:
println("Default")
}
}
select 在关闭的channel上发送
在这个场景中,我们向通过select的case分支来向关闭的channel发送数据,根据go channel的定义:会发生panic。
如下截图,我们的UT显示PASS,表示发生了Panic,当然你也可以改变一下把assert.Panics
注释掉,直接执行select的,那么你会得到第二张图的结果:"send on closed channel"
func TestSelect_ShouldPanic_WhenSendToClosedChannel(t *testing.T) {
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
ch3 := make(chan int, 1)
close(ch1)
close(ch2)
close(ch3)
assert.Panics(t, func() {
select {
case ch1 <- 1:
println("send ch1")
case ch2 <- 1:
println("send ch2")
case ch3 <- 1:
println("send ch3")
case <-time.After(10 * time.Second):
println("Timeout")
default:
println("Default")
}
})
}