本文节选翻译自https://go101.org/article/channel-use-cases.html

Go语言:使用Channel实现通知

通知可以被看作特殊的异步请求,这类请求的结果不重要,但是否完成很重要。一般来说,可以使用一个空的struct{}作为channel的元素类型。因为struct{}的大小是0,所以不怎么占用内存。

1对1的通知——通过Channel发送数据

如果channel里并没有接受到任何值,接收操作会阻塞,直到另一个goroutine发送了一个值给这个channel。所以通知方可以通过发送一个值给channel的方法,来通知另一个goroutine。而被通知方只需要等待接收同一个channel即可。

在以下例子中,名字叫donechannel被用来通知。

package main

import (
    "crypto/rand"
    "fmt"
    "os"
    "sort"
)

func main() {
    values := make([]byte, 32 * 1024 * 1024)
    if _, err := rand.Read(values); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }

    done := make(chan struct{}) // 阻塞与否都行

    // 用来排序的goroutine
    go func() {
        sort.Slice(values, func(i, j int) bool {
            return values[i] < values[j]
        })
        // 通知排序完成了。
        done <- struct{}{}
    }()

    // 干点别的……

    <- done // 等待接收通知
    fmt.Println(values[0], values[len(values)-1])
}

1对1的通知——通过Channel接收数据

如果一个channel缓冲区满了(一个阻塞的channel缓冲区永远是满的),发送请求操作会阻塞,直到另一个goroutine接收这个channel的请求。所以,通知方可以接收channel的数据,用以通知另一goroutine。而被通知方会往channel发送数据,直到发送成功。一般来说,这种channel是阻塞的。

比起上个例子,这种方法不太常用:

package main

import (
    "fmt"
    "time"
)

func main() {
    done := make(chan struct{})
        // 其实这里的缓冲区大小也可以设置为1。如果这样设置的话,需要
        // 先发送一个值,再创建下面的goroutine。

    go func() {
        fmt.Print("Hello")
        // 模拟延迟。
        time.Sleep(time.Second * 2)

        // 从done这个channel接收一个值,这样主goroutine就不
        // 再被发送操作阻塞。
        <- done
    }()

    // 这里会阻塞,等待通知。
    done <- struct{}{}
    fmt.Println(" world!")
}

事实上,以上两种1对1的通知方法,没有本质区别。都是快的操作等待被慢的操作通知。

N对1、1对N的通知

稍微扩展一下,就可以做N对1、1对N的通知:

package main

import "log"
import "time"

type T = struct{}

func worker(id int, ready <-chan T, done chan<- T) {
    <-ready // 阻塞在这里,等待被通知
    log.Print("Worker#", id, " starts.")
    // 模拟延迟
    time.Sleep(time.Second * time.Duration(id+1))
    log.Print("Worker#", id, " job done.")
    // 发送通知给主goroutine(N对1的通知)。
    done <- T{}
}

func main() {
    log.SetFlags(0)

    ready, done := make(chan T), make(chan T)
    go worker(0, ready, done)
    go worker(1, ready, done)
    go worker(2, ready, done)

    // 模拟初始化的时间。
    time.Sleep(time.Second * 3 / 2)
    // 发送1对N的通知。
    ready <- T{}; ready <- T{}; ready <- T{}
    // 等待接受N对1的通知。
    <-done; <-done; <-done
}

事实上,以上用法并不常见。实际应用中,我们常用sync.WaitGroup来做N对1的通知。而1对N的通知,可以直接通过关闭channel来实现。下面会仔细介绍。

通过关闭Channel,实现广播(1对N)通知

上面例子的1对N通知的用法现实中很少用到,因为有更好的方法。一个已经关闭的channel可以用来接受无数个值,我们可以根据这个特性来实现广播通知。

只需要把上个例子中的ready <- struct{}{}换成close(ready)即可。

...
    close(ready) // 广播通知
...

当然,就算1对1通知,也可以用关闭channel来实现。事实上,这也是非常常见的用法。

一个已经关闭的channel可以用来接受无数个值,这个特性会被用来做很多事情,我们以后会介绍。事实上,在标准库中,这个特性也经常被用到。比如context库用它来确认操作是否被取消。

定时通知

很容易通过channel进行定时通知:

package main

import (
    "fmt"
    "time"
)

func AfterDuration(d time.Duration) <- chan struct{} {
    c := make(chan struct{}, 1)
    go func() {
        time.Sleep(d)
        c <- struct{}{}
    }()
    return c
}

func main() {
    fmt.Println("Hi!")
    <- AfterDuration(time.Second)
    fmt.Println("Hello!")
    <- AfterDuration(time.Second)
    fmt.Println("Bye!")
}

事实上,time库中的After函数提供了类似的功能。(当然,它的实现效率高了很多。)我们应该尽量使用标准库中的函数,这样代码看着比较整洁。

注意,<-time.After(aDuration)会导致当前goroutine进入阻塞状态,而time.Sleep(aDuration)不会。

<-time.After(aDuration)经常会被用来处理超时,我们以后会详细介绍。