Golang 可以通过启动 goroutines 来并发执行任务。它们可以通过一种名为 “通道”的通信媒介相互通信。话不多说,下面我列举了几种不同情况下 channel 的使用以及其适用条件,最后总结出一张 channel table,能够帮助你快速吃透 channel 的所有要点。Let’t go!
Nil Channels
如果你像创建普通变量一样创建一个通道,通道将被初始化为零值。这里要提到的另一个重要细节是,通道的发送方和接收方都必须做好通信准备,否则我们将收到一个死锁错误。这可以通过缓冲通道来避免,我们将在后面讨论。
1 | // NOTE: Deadlock |
如果你在 main 中调用这个函数,你会得到以下错误和一些其他信息。1
fatal error: all goroutines are asleep - deadlock!
这是因为我们试图在往 nil channel上发送一个值,这是坚决不允许的操作。
Empty Channel
在以下代码中,我们将收到同样的死锁错误,因为我们正试图在 channel receiver 尚未准备好的通道上发送值。从下面的代码中可以看到,在接收器创建之前,值就已经在通道上发送了。
1 | // NOTE: Deadlock |
UN Buffered Channel
错误的代码已经够多了。这次让我给出一个有效的代码。在下面的代码中,我们编写了与上一个示例相同的代码,但不是创建一个 nil 通道,而是使用 make 函数创建了一个用默认值初始化的通道。 不过,如果在这种情况下没有接收器,就会错过结果,但不会产生死锁错误。1
2
3
4
5
6
7
8
9
10
11// NOTE: send single value through goroutine
func simpleChannel() {
var ch = make(chan int)
go func() {
ch <- 1
}()
// NOTE: if you comment this line. You will not be able to receive the result but code will not crash
fmt.Println(<-ch)
}
Channel 方向
默认情况下通道是双向的。下面的代码显示了一个只能用于发送值的通道。如果我们试图从中获取值,就会出错。1
2
3
4
5
6
7
8
9
10
11func uniDirectionalChannel() {
// Bidirectional [outside goroutine]
var ch = make(chan int)
go func(ch chan<- int) {
// unidirectinal [within goroutine]
ch <- 1
}(ch)
fmt.Println(<-ch)
}
Buffered Channel
这些通道可以像数组一样容纳多个值,因此在非缓冲通道中,如果我们尝试在没有接收器的情况下向其写入数据,就会出错,但在缓冲通道中,我们可以向其写入数据,直到缓冲区满为止。当缓冲区已满时,如果我们尝试向其中写入新值,就会出错。
如果我们这里不注释该函数的最后一行,就会出现死锁错误,因为这里将从空通道读取数据。
1 | // using buffered channel |
从通道读取数值还有另一种方法,即使用循环。在前面的示例中,我们逐个读取数值,但我们也可以通过循环读取接收通道中的数值,因此每当通道中发送一个新数值时,循环就会迭代,执行完正文中的代码后,就会等待下一个数值。如果接收器试图读取,但通道上已没有其他值,则会出现同样的死锁错误。
开发人员的职责是在通道使用后将其关闭,因为如果接收器试图读取已经关闭的通道,就会出现上述死锁问题。
Channels Table
我们已经讨论了通道的所有情况,但怎么才能快速记住它们呢? 别紧张😎,有我在,下表可以作为快速指南,对照着表盘一下我们写的 channel 就能避免出现死锁,并显示它们在不同情况下的行为。