通道(channel)是Go 语言的一种特色类型。利用通道可以在多个 goroutine 之间传递数据。通道类型的值是并发安全的,也是 Go 语言自带的、唯一一个可以满足并发安全性的类型。
通道的使用
在声明并初始化一个通道的时候,需要用到 Go 语言的内建函数make
。就像用make
初始化切片那样,我们传给这个函数的第一个参数应该是代表了通道的具体类型的类型字面量,此外,还可以接收一个int型的参数(可选),用来表示通道容量。
所谓通道的容量,就是指通道最多可以缓存多少个元素值。由此,虽然这个参数是
int
类型的,但是它是不能小于0
的。
当容量为0
时,我们可以称通道为非缓冲通道,也就是不带缓冲的通道。而当容量大于0
时,我们可以称为缓冲通道,也就是带有缓冲的通道。
通道相当于一个先进先出(FIFO)的队列。通道中的各个元素值都是严格地按照发送的顺序排列的,先被发送通道的元素值一定会先被接收。元素值的发送和接收都需要用到操作符<-
,也可以叫它接送操作符。
package main
import "fmt"
func main() {
// 初始化通道,通道的容量为 3
ch1 := make(chan int, 3)
// 连续地向该通道发送三个值,此时这三个值都会被缓存在通道之中
ch1 <- 2
ch1 <- 1
ch1 <- 3
elem1 := <-ch1
fmt.Printf("The first element received from channel ch1: %v\n",
elem1)
}
// 执行结果
// The first element received from channel ch1: 2
- 向通道发送元素值:使用接送操作符
<-
,把它写在变量名的右边,用于表达“向该通道发送一个元素值”的语义。- 例如:
ch1 <- 2
表达了“元素值将被发送到通道”这个语义。
- 例如:
- 从通道接收元素值:使用接送操作符
<-
,把它写在变量名的左边,用于表达“要从该通道接收一个元素值”的语义。- 例如:
elem1 := <-ch1
也叫接收表达式。在一般情况下,接收表达式的结果将会是通道中的一个元素值。
- 例如:
通道的基本特性
1. 对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的
在同一时刻,Go 语言的运行时系统只会执行同一通道的任意发送操作中的一个。直到这个元素值完全复制进通道后,其他针对该通道的发送操作才可能被执行。
类似的,在同一时刻,运行时系统也只会执行同一通道的任意接收操作中的一个。直到这个元素值完全被移出通道后,其他针对该通道的接收操作才可能被执行。即使这些操作是并发执行的也是如此。
另外,对通道中的同一元素值来说,发送操作和接收操作之间也是互斥的。例如,正在被复制进通道的元素值,是无法将它接收和取走。
注意:元素值从外界进入通道时会被复制。更具体地说,进入通道的并不是在接收操作符右边的那个元素值,而是它的副本。
另一方面,元素值从通道进入外界时会被复制。先生成通道中这个元素值的副本,发送给接受方,然后删除通道中这个元素值。
2. 发送操作和接收操作中对元素值的处理都是一步到位的
例如,发送操作要么还没复制元素值,要么已经复制完毕,绝不会出现只复制了一部分的情况。
又例如,接收操作在准备好元素值的副本之后,一定会删除掉通道中的原值,绝不会出现通道中仍有残留的情况。
这样既保证通道中元素值的完整性,也保证通道操作的唯一性。对于通道中的同一个元素值来说,它只可能是某一个发送操作放入的,同时也只可能被某一个接收操作取出。
3. 发送操作在完全完成之前会被阻塞。接收操作也是如此
发送操作包括了“复制元素值”和“放置副本到通道内部”这两个步骤。在这两个步骤完全完成之前,会阻塞在发送操作代码哪里,在它之后的代码不会被执行,直到阻塞解除。更细致地说,在通道完成发送操作之后,运行时系统会通知这句代码所在的 goroutine,以使它去争取继续运行代码的机会。
另外,接收操作通常包含了“复制通道内的元素值”“放置副本到接收方”“删掉原值”三个步骤。在所有这些步骤完全完成之前,发起该操作的代码也会一直阻塞,直到该代码所在的 goroutine 收到了运行时系统的通知并重新获得运行机会为止。
阻塞代码目的是为了实现操作的互斥和元素值的完整。
通道阻塞
缓冲通道:如果通道已满,那么对它的所有发送操作都会被阻塞,直到通道中有元素值被接收走。
这时,通道会通知最近发送操作所在的 goroutine 再次执行发送操作。由于发送操作在被阻塞后,它们所在的 goroutine 会顺序地进入通道内部的发送等待队列,所以通知的顺序总是公平的。
相对的,如果通道已空,那么对它的所有接收操作都会被阻塞,直到通道中有新的元素值出现。这时,通道会通知接收操作所在的 goroutine 再次执行接收操作。所有接收操作所在的 goroutine,都会按照先后顺序被放入通道内部的接收等待队列。
非缓冲通道:无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行,才会继续传递。非缓冲通道是在用同步的方式传递数据。也就是说,只有收发双方对接上了,数据才会被传递。
并且,数据是直接从发送方复制到接收方的,中间并不会用非缓冲通道做中转。相比之下,缓冲通道则在用异步的方式传递数据。
在大多数情况下,缓冲通道会作为收发双方的中间件。元素值会先从发送方复制到缓冲通道,之后再由缓冲通道复制给接收方。但是,当发送操作在执行的时候发现空的通道中有等待的接收操作,那么它会直接把元素值复制给接收方。
错误阻塞通道:对于值为nil
的通道,不论它的具体类型是什么,对它的发送操作和接收操作都会永久地处于阻塞状态。它们所属的 goroutine 中的任何代码,都不再会被执行。
注意,由于通道类型是引用类型,所以它的零值就是