Go by Example - Select, Timeouts, Non-Blocking Channel Operations, Closing Channels and Range over Channels

Go by Example - Select, Timeouts, Non-Blocking Channel Operations, Closing Channels and Range over Channels

介绍go中的select, 超时, 非阻塞通道操作, 关闭通道和通道范围

Select

Go 的 select 可以让你在多个通道操作中等待。将 goroutines 和通道与 select 结合起来是 Go 的一项强大功能。

select.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"fmt"
"time"
)

func main() {
// 在我们的示例中,我们将跨两个通道进行选择
c1 := make(chan string)
c2 := make(chan string)

// 每个通道将在一定时间后收到一个值,以模拟在并发程序中执行的阻塞 RPC 操作
go func() {
time.Sleep(1 * time.Second)
c1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
c2 <- "two"
}()

// 我们将使用 select 同时等待这两个值,并在每个值到达时打印出来
for i := 0; i < 2; i++ {
select {
case msg1 := <-c1:
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
}
}

我们收到的数值是 "one",然后是 "two"。

log
1
2
3
4
5
$ time go run select.go 
received one
received two

real 0m2.245s

请注意,由于 1 秒睡眠和 2 秒睡眠同时执行,因此总执行时间约为 2 秒。

Timeouts

超时对于连接外部资源或需要限制执行时间的程序来说非常重要。借助通道和选择,在 Go 中实现超时既简单又优雅。

timeouts.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
"fmt"
"time"
)

func main() {
// 在我们的示例中,假设我们正在执行一个外部调用,该调用会在 2 秒后通过通道 c1 返回结果
// 请注意,通道是缓冲的,因此 goroutine 中的发送是非阻塞的
// 这是一种常见的模式,以防止在通道从未被读取的情况下发生例行程序泄漏
c1 := make(chan string, 1)
go func() {
time.Sleep(2 * time.Second)
c1 <- "result 1"
}()

// res := <-c1 等待结果,<-time.After 等待超时 1 秒后发送的值
// 由于 select 会从第一个准备就绪的接收开始,因此如果操作时间超过允许的 1 秒,我们就会采用超时情况
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(1 * time.Second):
fmt.Println("timeout 1")
}

// 如果我们允许更长的 3 秒超时,那么从 c2 接收就会成功,我们将打印结果
c2 := make(chan string, 1)
go func() {
time.Sleep(2 * time.Second)
c2 <- "result 2"
}()
select {
case res := <-c2:
fmt.Println(res)
case <-time.After(3 * time.Second):
fmt.Println("timeout 2")
}
}

运行该程序后,第一个操作超时,第二个操作成功。

log
1
2
3
$ go run timeouts.go
timeout 1
result 2

Non-Blocking Channel Operations

通道上的基本发送和接收都是阻塞的。不过,我们可以使用带有默认子句的 select 来实现非阻塞的发送和接收,甚至是非阻塞的多向选择。

non-blocking-channel-operations.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import "fmt"

func main() {
messages := make(chan string)
signals := make(chan bool)

// 这是一个非阻塞接收器
// 如果信息中有可用的值,select 将使用该值的 <-messages 情况
// 如果没有,则会立即使用默认情况
select {
case msg := <-messages:
fmt.Println("received message", msg)
default:
fmt.Println("no message received")
}

// 非阻塞发送的工作原理与此类似
// 这里 msg 无法发送到消息通道,因为通道没有缓冲区,也没有接收器。因此选择默认情况
msg := "hi"
select {
case messages <- msg:
fmt.Println("sent message", msg)
default:
fmt.Println("no message sent")
}

// 我们可以使用默认子句上方的多种情况来实现多向非阻塞选择
// 在这里,我们尝试对消息和信号进行非阻塞接收
select {
case msg := <-messages:
fmt.Println("received message", msg)
case sig := <-signals:
fmt.Println("received signal", sig)
default:
fmt.Println("no activity")
}
}
log
1
2
3
4
$ go run non-blocking-channel-operations.go
no message received
no message sent
no activity

Closing Channels

关闭通道表示不再发送任何值。这对于向通道的接收者传达完成信息非常有用。

closing-channels.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main

import "fmt"

// 在本例中,我们将使用 jobs 通道将 main() goroutine 要完成的工作传递给 Worker goroutine
// 当 Worker 没有工作时,我们将关闭作业通道
func main() {
jobs := make(chan int, 5)
done := make(chan bool)

// 下面是 Worker goroutine。它以 j, more := <-jobs 的形式重复接收来自作业的信息
// 在这种特殊的 2 值接收形式中,如果 jobs 已关闭,且通道中的所有值都已接收完毕,则 more 值将为 false
// 当我们完成所有工作时,我们会用它来通知 done
go func() {
for {
j, more := <-jobs
if more {
fmt.Println("received job", j)
} else {
fmt.Println("received all jobs")
done <- true
return
}
}
}()

// 这会通过 jobs 通道向 Worker 发送 3 个job,然后关闭它
for j := 1; j <= 3; j++ {
jobs <- j
fmt.Println("sent job", j)
}
close(jobs)
fmt.Println("sent all jobs")

// 我们使用之前看到的同步方法来等待 Worker
<-done

// 从关闭的通道读取数据会立即成功,并返回底层类型的零值
// 如果接收到的值是通过成功发送操作传送到通道的,则第二个可选返回值为 true
// 如果接收到的值是因通道关闭且为空而生成的零值,则第二个可选返回值为 false
_, ok := <-jobs
fmt.Println("received more jobs:", ok)
}
log
1
2
3
4
5
6
7
8
9
10
$ go run closing-channels.go
sent job 1
received job 1
sent job 2
received job 2
sent job 3
received job 3
sent all jobs
received all jobs
received more jobs: false

关闭通道的概念很自然地引出了我们的下一个例子:通道范围。

Range over Channels

在前面的示例中,我们看到了 for 和 range 如何提供对基本数据结构的迭代。我们也可以使用这种语法迭代从通道接收到的值。

range-over-channels.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
// 我们将遍历队列通道中的 2 个值
queue := make(chan string, 2)
queue <- "one"
queue <- "two"
close(queue)

// 这个范围会迭代从队列中接收到的每个元素
// 因为我们关闭了上面的通道,所以迭代在接收到 2 个元素后终止
for elem := range queue {
fmt.Println(elem)
}
}
log
1
2
3
$ go run range-over-channels.go
one
two

这个例子还表明,可以关闭一个非空通道,但仍能接收到剩余的值。

参考链接

Go by Example - Select, Timeouts, Non-Blocking Channel Operations, Closing Channels and Range over Channels

https://blog.wty.cool/2024/03/10/go_by_example/Select-Timeouts-Non-Blocking_Channel_Operations-Closing_Channels-Range_over_Channels/

作者

孤独小狼

发布于

2024-03-10

更新于

2024-03-10

许可协议

评论