大家好,我是江湖十年。
今天继续翻译 Go 官方博客。这次是 2010 年发表的《Go Concurrency Patterns: Timing out, moving on》,文中用两个简短的例子,展示了如何借助 channel、select 和 goroutine 实现超时,以及如何处理并发查询中的竞态条件。本文是对这篇文章的中文翻译。
如下为译文:
Andrew Gerrand
2010 年 9 月 23 日
并发编程有其自己的惯用法。超时就是一个很好的例子。虽然 Go 的 channel 并不直接支持超时,但实现起来很容易。假设我们想从 channel ch 接收值,但最多只愿意等待一秒。我们可以先创建一个用于发信号的 channel,并启动一个 goroutine,让它在休眠一段时间后再向该 channel 发送值:
1 | timeout := make(chan bool, 1) |
然后,我们可以使用 select 语句从 ch 或 timeout 中接收值。如果一秒后 ch 上仍没有值到达,就会选中超时分支,并放弃从 ch 读取的尝试。
1 | select { |
timeout channel 带有可容纳 1 个值的缓冲区,这让超时 goroutine 能够向该 channel 发送值,然后退出。这个 goroutine 并不知道(也不关心)值是否被接收。这意味着,如果 ch 的接收在超时到达之前就发生了,这个 goroutine 也不会永远滞留。timeout channel 最终会被垃圾回收器释放。
(在这个例子中,我们使用 time.Sleep 来演示 goroutines 和 channels 的工作机制。在实际程序中,你应该使用 time.After,这个函数会返回一个 channel,并在指定的时长过后向该 channel 发送值。)
我们来看这个模式的另一种变体。在这个例子中,程序会同时从多个副本数据库中读取数据。程序只需要其中一个答案,并且应该接受最先到达的那个答案。
Query 函数接收一个数据库连接切片和一个 query 字符串。它会并行查询每个数据库,并返回最先收到的响应:
1 | func Query(conns []Conn, query string) Result { |
在这个例子中,闭包执行的是非阻塞发送,这是通过把发送操作放进带有 default 分支的 select 语句实现的。如果发送不能立即完成,就会选中 default 分支。让发送变成非阻塞,可以保证循环中启动的 goroutines 都不会滞留。然而,如果结果在主函数执行到接收操作之前就到达了,由于此时还没有人准备好接收,这次发送就可能会失败。
这个问题是所谓竞态条件的典型示例,但修复起来很简单。我们只需确保 channel ch 带有缓冲区(把缓冲区长度作为第二个参数传给 make),从而保证第一个发送有地方存放值。这样就能确保发送总是成功,而且无论执行顺序如何,最先到达的值都会被取回。
这两个例子展示了 Go 表达 goroutines 之间复杂交互是多么简单。
原文完。
希望此文能对你有所启发。
延伸阅读
- Go Concurrency Patterns: Timing out, moving on 原文:https://go.dev/blog/concurrency-timeouts
- Go Concurrency Patterns: Pipelines and cancellation 译文:https://jianghushinian.cn/2026/11/04/go-concurrency-patterns-pipelines/
- Go Concurrency Patterns: Context 译文:https://jianghushinian.cn/2026/11/03/go-concurrency-patterns-context/
- Go Concurrency Patterns 演讲:https://go.dev/talks/2012/concurrency.slide
- 本文永久地址:https://jianghushinian.cn/2026/11/05/go-concurrency-patterns-timeouts/
联系我
- 公众号:Go编程世界
- 微信:jianghushinian
- 邮箱:jianghushinian007@outlook.com
- 博客:https://jianghushinian.cn
- GitHub:https://github.com/jianghushinian