有时你可能会发现自己考虑将一个或多个done通道合并到一个done通道中,该通道在任何组件通道关闭时关闭。编写一个执行这种耦合度较高的select语句是可行的,尽管很冗长;但是有时你无法知道运行状态下done通道的数量。在这种情况下,或者你如果喜欢单线操作,你可以使用or通道模式将这些通道组合在一起(如果你对done通道看着有点懵,可以先看看上一节)。 这种模式使用递归和goroutine创建一个复合done通道。 我们来看一下: ``` var or func(channels ...<-chan interface{}) <-chan interface{} or = func(channels ...<-chan interface{}) <-chan interface{} { //1 switch len(channels) { case 0: //2 return nil case 1: //3 return channels[0] } orDone := make(chan interface{}) go func() { //4 defer close(orDone) switch len(channels) { case 2: //5 select { case <-channels[0]: case <-channels[1]: } default: //6 select { case <-channels[0]: case <-channels[1]: case <-channels[2]: case <-or(append(channels[3:], orDone)...): //6 } } }() return orDone } ``` 1. 这里我们建立了名为or的函数,接收数量可变的通道并返回单个通道。 2. 由于这是个递归函数,我们必须设置终止条件。第一个条件是,如果传入的切片是空的,我们简单的返回一个nil通道。这与不传递通道的想法一致:我们不希望复合通道做任何事。 3. 第二个递归终止条件是,如果切片只含有一个元素,我们就返回给元素。 4. 这是该函数最重要的部分,也是递归产生的地方。我们建立一个goroutine,以便可以不受阻塞地等待我们通道上的消息。 5. 由于我们这里是递归的,每次递归调用将至少有两个通道。作为保持goroutine数量受到限制的优化方法,们在这里为仅使用两个通道的时设置了一个特殊情况。 6. 在这里,我们递归地在第三个索引之后,从我们切片中的所有通道中创建一个or通道,然后从中选择。递归操作会逐层累计直到取到第一个通道元素。我们在其中传递了orDone通道,这样当该树状结构顶层的goroutines退出时,结构底层的goroutines也会退出。 这是一种奇妙的做法,你可以将任意数量的通道组合到单个通道中,只要任何作为组件的通道关闭或被写入,整个通道就会关闭。让我们来看看该如何进行实际操作。下面这个例子将经过一段时间后关闭通道,然后使用or函数将这些通道合并到一个关闭的通道中: ``` sig := func(after time.Duration) <-chan interface{} { //1 c := make(chan interface{}) go func() { defer close(c) time.Sleep(after) }() return c } start := time.Now() //2 <-or(sig(2*time.Hour), sig(5*time.Minute), sig(1*time.Second), sig(1*time.Hour), sig(1*time.Minute)) fmt.Printf("done after %v", time.Since(start)) //3 ``` 1. 此功能只是创建了一个通道,当后续时间中指定的时间结束时将关闭该通道。 2. 在这里,我们设置追踪自or函数的通道开始阻塞的起始时间。 3. 在这里我们打印阻塞发生的时间。 这会输出: ``` done after 1.000216772s ``` 请注意,尽管在我们的调用中放置了多个通道需要多个时间才能关闭,但我们在一秒钟后关闭的通道会导致由该d调用创建的整个通道关闭。 这是因为它位于树或函数构建的树中,它将始终第一个关闭,因此依赖于其关闭的通道也将关闭。 我们以额外创建 f(x)=x/2 个goroutine以"简洁的"实现该目的,其中x是goroutine的数量。请记住Go的一个优点是能够快速创建,调度和运行goroutines,并且 该语言积极鼓励使用goroutines来正确建模问题。无需在前期太担心在这里创建的分支太多。如果在编译时你不知道自己正在使用多少个done通道,那么恐怕就没有其他更好的方法来合并done通道了。 这种模式适用于系统中模块的交叉点。在这些交叉点,有多种条件通过你的调用堆栈取消goroutines树。 使用or函数,你可以简单地将它们组合在一起并将其传递给堆栈。 我们将在“context包”中看到另一种更具描述性的做法。 我们也将看到这种模式的变体在第五章“重复请求”中形成更复杂的模式。 * * * * * 学识浅薄,错误在所难免。我是长风,欢迎来Golang中国的群(211938256)就本书提出修改意见。