go语言之select指南
想象一下场景:
你现在是一位咖啡师,你的面前有多个顾客在不同的窗口等待点单(每个窗口可以看作一个 channel)。你不可能同时处理所有窗口的请求,但你希望尽快处理到来的请求。select 语句就像你的耳朵和眼睛,让你能够同时监听所有窗口,一旦有顾客按下呼叫铃(channel 有数据可接收),你就可以优先处理那个窗口的请求。
select 语句的基本结构:
1 | select { |
核心要点:
- 多路复用:
select允许你同时等待多个 channel 的操作(发送或接收)。 - 非阻塞:
select本身不会阻塞。它会检查所有的case,如果其中一个case满足条件(可以进行收发操作),就执行该case对应的代码。 - 随机选择: 如果有多个
case同时满足条件,Go 语言会随机选择一个case执行。 default子句(可选): 如果没有case准备好,并且存在default子句,那么会执行default中的代码。如果不存在default,select语句会阻塞,直到至少有一个case可以执行。
生动解释每个 case:
case <-chan1::这表示尝试从通道chan1接收数据。如果chan1中有数据,这个case就会被选中,并且接收到的数据会被丢弃(因为你没有用变量接收)。你可以把它想象成你听到一个窗口的铃响了,你知道有人要点单了。case val := <-chan2::这同样是尝试从通道chan2接收数据。如果chan2中有数据,这个case会被选中,并且接收到的数据会存储在变量val中。这就像你听到另一个窗口的铃响了,并且你知道了顾客想要点什么。case chan3 <- expr::这表示尝试向通道chan3发送数据expr。如果chan3有足够的空间(非满),这个case就会被选中,并且expr会被发送到chan3。这就像你准备好了一杯咖啡,准备送到一个等待的窗口。default::如果没有任何顾客按下铃(没有任何 channel 可读或可写),并且你不想一直等待,你可以使用default子句来做一些其他的事情,比如擦桌子或者看看还有哪些咖啡豆。
实际应用示例代码:
让我们模拟一个场景,有两个不同的服务在向我们的主程序发送消息。我们希望能够同时监听这两个服务,并处理先到达的消息。
1 | package main |
代码解释:
- 我们创建了两个通道
ch1和ch2,分别用于接收来自service1和service2的消息。 service1和service2Goroutine 会不断地生成消息并通过各自的通道发送。它们发送消息的时间间隔是随机的,模拟了不同的服务可能在不同的时间发送数据。- 在
main函数的for循环中,我们使用select语句同时监听ch1和ch2。 - 哪个通道先有数据到达,对应的
case就会被执行,主程序会接收并打印该消息。 - 由于
select的随机性,你每次运行程序,接收到的消息顺序可能会不同。
更进一步的应用场景:
- 超时控制: 你可以在
select中加入一个time.After的case,如果在指定时间内没有从其他通道接收到数据,就可以执行超时处理逻辑。 - 取消操作: 可以使用一个 done channel,在需要取消某个 Goroutine 的时候关闭这个 channel,然后在
select中监听这个 done channel,一旦关闭就退出 Goroutine。 - 多路网络请求: 同时等待多个网络请求的响应,哪个先返回就先处理哪个。
1. 超时控制
场景: 假设我们正在尝试从一个外部服务获取数据,但这个服务有时响应很慢或者没有响应。我们不希望程序一直等待下去,而是希望在一定时间后放弃并进行其他处理。
实现方式: 在 select 语句中加入一个 time.After(timeout) 的 case。time.After(timeout) 会返回一个在 timeout 时间后会收到一个 time.Time 值的 channel。
示例代码:
1 | package main |
代码解释:
fetchData函数模拟了一个需要 2 秒才能完成的操作,并将结果发送到resultchannel。- 在
main函数中,我们启动了一个 Goroutine 来执行fetchData。 - 我们设置了一个
timeout为 1 秒。 select语句同时监听resultchannel 和time.After(timeout)返回的 channel。- 如果
fetchData在 1 秒内完成,我们会从result接收到数据并打印。 - 如果超过 1 秒后,
time.After(timeout)返回的 channel 会收到一个值,case <-time.After(timeout):就会被选中,我们打印 “请求超时,放弃等待。”。
运行结果(可能):
由于 fetchData 需要 2 秒,而超时时间是 1 秒,所以很可能会输出:
1 | 请求超时,放弃等待。 |
如果你将 time.Sleep(2 * time.Second) 改为一个小于 1 秒的值,你就会看到成功获取数据的输出。
2. 取消操作
场景: 有时我们需要在某个操作进行到一半的时候取消它,例如用户关闭了一个页面,我们不再需要等待后台任务完成。
实现方式: 使用一个 “done” channel。当需要取消操作时,关闭这个 done channel。在执行任务的 Goroutine 中,使用 select 监听这个 done channel。一旦 done channel 被关闭,Goroutine 就可以清理资源并退出。
示例代码:
1 | package main |
代码解释:
worker函数在一个无限循环中模拟工作。- 它使用
select同时监听donechannel 和默认情况。 - 如果
donechannel 接收到值(当它被关闭时,会一直可以接收到零值),则case <-done:会被选中,worker 打印退出信息并返回。 defaultcase 表示如果没有收到取消信号,worker 就继续工作一段时间。- 在
main函数中,我们创建了一个donechannel 并启动了workerGoroutine,并将donechannel 传递给它。 - 主程序等待 2 秒后,通过
close(done)关闭了donechannel,向worker发送取消信号。 worker接收到信号后会退出。
运行结果(大致):
1 | 启动工作者... |