Don’t communicate by sharing memory; share memory by communicating. (不要通过共享内存来通信,而应该通过通信来共享内存。)
通道类型是后半句话的完美实现,我们可以利用通道在多个 goroutine 之间传递数据。
底层存储是 环形链表

初识

通道类型的值本身就是并发安全的,这也是 Go 语言自带的、唯一一个可以满足并发安全性的类型。
在声明并初始化一个通道的时候,我们需要用到 Go 语言的内建函数make。就像用make初始化切片那样,我们传给这个函数的第一个参数应该是代表了通道的具体类型的类型字面量。 比如,类型字面量chan int,其中的chan是表示通道类型的关键字,而int则说明了该通道类型的元素类型。又比如,chan string代表了一个元素类型为string的通道类型。
在初始化通道的时候,make函数除了必须接收这样的类型字面量作为参数,还可以接收一个int类型的参数。后者是可选的,用于表示该通道的容量。所谓通道的容量,就是指通道最多可以缓存多少个元素值。由此,虽然这个参数是int类型的,但是它是不能小于0的。
当容量为0时,我们可以称通道为非缓冲通道,也就是不带缓冲的通道。而当容量大于0时,我们可以称为缓冲通道,也就是带有缓冲的通道。非缓冲通道和缓冲通道有着不同的数据传递方式。
一个通道相当于一个先进先出(FIFO)的队列。也就是说,通道中的各个元素值都是严格地按照发送的顺序排列的,先被发送通道的元素值一定会先被接收。元素值的发送和接收都需要用到操作符<-。我们也可以叫它接送操作符。一个左尖括号紧接着一个减号形象地代表了元素值的传输方向。

  1. package main
  2. import "fmt"
  3. func main() {
  4. ch1 := make(chan int, 3)
  5. ch1 <- 2
  6. ch1 <- 1
  7. ch1 <- 3
  8. elem1 := <-ch1
  9. fmt.Printf("The first element received from channel ch1: %v\n",
  10. elem1)
  11. }

当我们需要从通道接收元素值的时候,同样要用接送操作符<-,只不过,这时需要把它写在变量名的左边,用于表达“要从该通道接收一个元素值”的语义。比如:<-ch1,这也可以被叫做接收表达式。

单向通道

定义

所谓单向通道就是,只能发不能收,或者只能收不能发的通道。一个通道是双向的,还是单向的是由它的类型字面量体现的。

  1. var uselessChan = make(chan<- int, 1)
  2. // chan右边的那个<-,这表示了这个通道是单向的,并且只能发而不能收。 发送通。
  3. // 操作符紧挨在chan的左边,那么就说明该通道只能收不能发。接收通道。

通道的发送和接收基本的特性

互斥:同一个通道 发送操作 或 接收操作 之间是互斥的

在同一时刻,Go 语言的运行时系统(以下简称运行时系统)只会执行对同一个通道的任意个发送操作中的某一个。直到这个元素值被完全复制进该通道之后,其他针对该通道的发送操作才可能被执行
类似的,在同一时刻,运行时系统也只会执行,对同一个通道的任意个接收操作中的某一个。直到这个元素值完全被移出该通道之后,其他针对该通道的接收操作才可能被执行。
即使这些操作是并发执行的也是如此。这里所谓的并发执行,你可以这样认为,多个代码块分别在不同的 goroutine 之中,并有机会在同一个时间段内被执行。
另外,对于通道中的同一个元素值来说,发送操作和接收操作之间也是互斥的。例如,虽然会出现,正在被复制进通道但还未复制完成的元素值,但是这时它绝不会被想接收它的一方看到和取走。这里要注意的一个细节是,元素值从外界进入通道时会被复制。
更具体地说,进入通道的并不是在接收操作符右边的那个元素值,而是它的副本。另一方面,元素值从通道进入外界时会被移动。这个移动操作实际上包含了两步,第一步是生成正在通道中的这个元素值的副本,并准备给到接收方,第二步是删除在通道中的这个元素值。

不可分割:发送操作和接收操作中对元素值的处理都是不可分割的

它们处理元素值时都是一气呵成的,绝不会被打断。
例如,发送操作要么还没复制元素值,要么已经复制完毕,绝不会出现只复制了一部分的情况。又例如,接收操作在准备好元素值的副本之后,一定会删除掉通道中的原值,绝不会出现通道中仍有残留的情况。
这既是为了保证通道中元素值的完整性,也是为了保证通道操作的唯一性。对于通道中的同一个元素值来说,它只可能是某一个发送操作放入的,同时也只可能被某一个接收操作取出。

阻塞:发送接收操作在完全完成之前会被阻塞

一般情况下,发送操作包括了“复制元素值”和“放置副本到通道内部”这两个步骤。在这两个步骤完全完成之前,发起这个发送操作的那句代码会一直阻塞在那里。也就是说,在它之后的代码不会有执行的机会,直到这句代码的阻塞解除。更细致地说,在通道完成发送操作之后,运行时系统会通知这句代码所在的 goroutine,以使它去争取继续运行代码的机会。
另外,接收操作通常包含了“复制通道内的元素值”“放置副本到接收方”“删掉原值”三个步骤。在所有这些步骤完全完成之前,发起该操作的代码也会一直阻塞,直到该代码所在的 goroutine 收到了运行时系统的通知并重新获得运行机会为止。
阻塞代码其实就是为了实现操作的互斥和元素值的完整。

发送和接收操作可能被长时间的阻塞的原因

  1. 缓冲通道如果通道已满,那么对它的所有发送操作都会被阻塞,直到通道中有元素值被接收走。(这时,通道会优先通知最早因此而等待的、那个发送操作所在的 goroutine,后者会再次执行发送操作)
  2. 缓冲通道如果通道已空,那么对它的所有接收操作都会被阻塞,直到通道中有新的元素值出现。这时,通道会通知最早等待的那个接收操作所在的 goroutine,并使它再次执行接收操作。(因此而等待的、所有接收操作所在的 goroutine,都会按照先后顺序被放入通道内部的接收等待队列。)
  3. 非缓冲通道无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行,才会继续传递。由此可见,非缓冲通道是在用同步的方式传递数据。也就是说,只有收发双方对接上了,数据才会被传递。
  4. 值为nil的通道,不论它的具体类型是什么,对它的发送操作和接收操作都会永久地处于阻塞状态。它们所属的 goroutine 中的任何代码,都不再会被执行。(没有make初始化)

发送和接收操作引发 panic的原因

  1. 通道关闭,再对它进行发送操作,就会引发 panic。
  2. 关闭一个已经关闭了的通道,也会引发 panic。注意,接收操作是可以感知到通道的关闭的,并能够安全退出。

注意,如果通道关闭时,里面还有元素值未被取出,那么接收表达式的第一个结果,仍会是通道中的某一个元素值,而第二个结果值一定会是true。