Go的秘密生活:select 语句
Source: Dev.to
如何阻止快速数据等待慢速通道
第 25 部分:多路复用器、超时和非阻塞读取
Ethan 正在观看终端输出一行行滴下来。速度慢得令人痛苦。
“我不明白,”他揉着眼睛说。“我有两个 goroutine 在发送数据。一个是本地缓存,返回时间为一毫秒。另一个是网络调用,需要五秒。但快速数据却在等待慢速数据。”
Eleanor 走过去查看他的代码。
问题代码
func process(cacheChan “You have created a traffic jam,” Eleanor observed. “Channel receives are blocking. Because you asked for `netChan` first, your function halts right there. It doesn’t matter that `cacheChan` has been ready for 4.99 seconds. You are forcing a sequential read on concurrent data.”
我该如何读取先准备好的那个? Ethan 问。
你需要一个多路复用器。 在 Go 中,我们使用 select 语句。
select 语句
Eleanor 重写了他的函数。select 语句看起来和 switch 完全一样,但它只针对通道操作。
解决方案
func process(cacheChan “Exactly,” Eleanor said. “`select` listens to all its cases simultaneously. Whichever channel is ready first, it executes that case. If multiple channels are ready at the same time, Go picks one completely at random to ensure fairness.”
超时(time.After)
Ethan 想知道如果网络宕机,netChan 永远不发送任何东西会怎样。
“目前,”Eleanor 回答,“你的
select会永远等待。这会导致 goroutine 泄漏。在生产代码中必须强制超时。”
添加超时
func processWithTimeout(cacheChan “`time.After` acts as a ticking time bomb,” Eleanor explained. “If `netChan` or `cacheChan` don’t respond within two seconds, the timeout case wins the race.”
安全提示:time.After 会创建一个计时器,直到触发前一直存在。如果你把它放在一个快速、紧凑的循环中,会导致内存泄漏。这种情况下请使用 time.NewTimer 并显式 Stop()。对于更复杂的多层超时,建议使用 context.WithTimeout(如第 22 集所示)。
非阻塞读取(default)
“如果我根本不想等待怎么办?我只想窥探一下通道,如果有数据就取走,没有的话立刻去做别的事,”Ethan 询问。
“这时就使用
default分支,”Eleanor 微笑着说。
示例
func checkStatus(statusChan “A `select` with a `default` case is completely non‑blocking,” she explained. “If no channels are ready that exact microsecond, it falls through to the `default` block immediately.”
Ethan 靠在椅背上说:“所以我可以一次等待多个事件,设定时间限制,甚至完全不等待。”
“正是如此,”Eleanor 说。“你不再受 goroutine 的摆布,而是对它们进行编排。”
关键概念
select 语句
- 一种控制结构,允许 goroutine 在多个通信操作上等待。
- 它会阻塞,直到其中一个 case 可以执行。
- 如果有多个 case 已就绪,则随机选择一个执行。
空 select
select {}(没有任何 case)会永久阻塞当前 goroutine。
超时与内存
time.After(duration)适用于简单、一次性的超时。- 生产环境警告: 在紧密循环中,使用
time.NewTimer(duration)并调用.Stop()以避免内存泄漏。 - 对于复杂的、多层次的超时,推荐使用
context.WithTimeout。
循环标签
- 若要在
select内部跳出for循环,必须使用标签(例如break outer)。普通的break只能退出select。
非阻塞操作(default)
- 添加
defaultcase 会使select变为非阻塞。如果没有其他通道就绪,它会立即执行。
下一集:工作池 – Ethan 学习如何并发处理成千上万的任务。
- 使用固定数量的 goroutine 来防止内存耗尽。
- Aaron Rose 是 tech-reader.blog 的软件工程师和技术作者,也是《Think Like a Genius》的作者,详情请见 Think Like a Genius。