翻译自 Go Blog。
原文地址:https://blog.golang.org/go-concurrency-patterns-timing-out-and

并发编程有自己的一些习惯用语,超时就是其中之一。虽然 Golang 的管道并没有直接支持超时,但是实现起来并不难。假设遇到了这样一种场景:在从 管道 ch 中取值之前至少等待 1 秒钟。我们可以创建一个管道用来传递信号,开启一个协程休眠一秒钟,然后给管道传递一个值。

timeout := make(chan bool, 1)
go func() {
    time.Sleep(1 * time.Second)
    timeout <- true
}()

然后就可以使用一个 select 语句来从 timeout 或者 ch 管道中获取数据。如果 ch 管道在 1 秒钟之后还没有返回数据,超时的判断条件就会触发 ch 的读操作将会被抛弃掉。

select {
    case <-ch:
    // a read from ch has occurred
    case <-timeout:
    // the read from ch has timed out
}

timeout 管道的缓冲区空间为 1,因此 timeout 协程将会在发送消息到管道之后退出执行。协程并不知道(也不关心)管道中的值是否被接受。因此,即使 ch 管道先于 timeout 管道返回了,timeout 协程也不会永久等待。timeout 管道最终会被垃圾回收机制回收掉。

(在上面的示例中我们使用了 time.Sleep 方法来演示协程和管道的机制,但是在真实的代码中应该用 time.After 方法,该方法返回了一个管道,并且会在参数指定的时间之后向管道中写入一个消息)

下面我们来看这种模式的另外一个变种。我们需要从多个分片数据库中同时取数据,程序只需要其中最先返回的那个数据。

下面的 Query 方法接受两个参数:一个数据库链接的切片和一个数据库查询语句。该方法将平行查询所有数据库并返回第一个接受到的响应结果。

func Query(conns []Conn, query string) Result {
    ch := make(chan Result, 1)
    for _, conn := range conns {
        go func(c Conn) {
            select {
            case ch <- c.DoQuery(query):
            default:
            }
        }(conn)
    }
    return <-ch
}

在上面这个例子中,go 关键字后的闭包实现了一个非阻塞式的查询请求,因为 DoQuery 方法被放到了带 default 分支的 select 语句中。假如 DoQuery 方法没有立即返回,default 分支将会被选中执行。让查询请求非阻塞保证了 for 循环中创建的协程不会一直阻塞。另外,假如在主方法从 ch 管道中取出值并返回结果之前有第二个查询结果返回了,管道 ch 的写操作将会失败,因为管道并未就绪。

上面描述的问题其实是“竞争关系”的一种典型例子,示例代码只是一种很通俗的解决方案。我们只是中管道上设定了缓冲区(通过在管道的 make 方法中传入第二个参数),保证了第一个写入者能够有空间来写入值。这种策略保证了管道的第一次写入一定会成功,无论代码以何种顺序执行,第一个写入的值将会被当作最终的返回值。

Go 的协程之前可以进行复杂的协作,以上两个例子就是最简单的证明。