Channel
Don’t communicate by sharing memory; share memory by communicating. (不要通过共享内存来通信,而应该通过通信来共享内存。)
以上是作为 Go 语言的主要创造者之一的 Rob Pike 的至理名言,也充分体现了 Go 语言最重要的编程理念。通道类型作为和Go程并驾齐驱的另一种数据类型,是后半句话的完美实现,我们可以利用通道在个goroutine 之间传递数据。
通道的基本知识
安全性
通道类型是Go 语言自带的、唯一一个可以满足并发安全性的类型。
顺序性
一个通道本质上是一个先进先出(FIFO)的队列。所以通道中的各个元素值都是严格地按照发送的顺序排列的,先被发送通道的元素值一定会先被接收。元素值的发送和接收都需要用到操作符<-。我们也可以叫它接送操作符。一个左尖括号紧接着一个减号形象地代表了元素值的传输方向。
声明参数
在声明通道类型变量时我们需要使用内联函数 make(var type, int length)。第一个参数是用于声明该通道支持传递的参数类型,第二个参数则是通道的容量:
ch1 := make(chan int, 3)
缓冲通道与非缓冲通道
当通道容量为0时,我们称通道为非缓冲通道,而当容量大于0时,我们称为缓冲通道,也就是带有缓冲的通道。非缓冲通道和缓冲通道有着不同的数据传递方式。
通道发送和接收操作的基本特性
- 对于同一个通道的发送和接收的操作都是是互斥的。
发送
在同一时刻,Go 语言的运行时系统(以下简称运行时系统)只会执行对同一个通道的任意个发送操作中的某一个。
直到这个元素值被完全复制进该通道之后,其他针对该通道的发送操作才可能被执行。
接收
类似的,在同一时刻,运行时系统也只会执行,对同一个通道的任意个接收操作中的某一个。
直到这个元素值完全被移出该通道之后,其他针对该通道的接收操作才可能被执行。即使这些操作是并发执。行的也是如此
- 发送和接收操作中对元素的处理都是一气呵成的,不可切割的。
发送操作要么还没复制元素值,要么已经复制完毕,绝不会出现只复制了一部分的情况。
接收操作在准备好元素值的副本之后,一定会删除掉通道中的原值,绝不会出现通道中仍有残留的情况。
- 发送和接收操作在完成之前都会被阻塞, 阻塞代码其实就是为了实现操作的互斥和元素值的完整。
发送操作包括了“复制元素值”和“放置副本到通道内部”这两个步骤,在这两个步骤完全完成之前,发起这个发送操作的那句代码会一直阻塞在那里。
接收操作通常包含了 复制通道内的元素值 放置副本到接收方 删掉原值 三个步骤。在所有这些步骤完全完成之前,发起该操作的代码也会一直阻塞,直到该代码所在的goroutine 收到了运行时系统的通知并重新获得运行机会为止。
- 发送和接收操作实际传递的是元素的副本值。
发送
元素值从外界进入通道时会被复制。更具体地说,进入通道的并不是在接收操作符右边的那个元素值,而是它的副本。
接收
另一方面,元素值从通道进入外界时会被移动。这个移动操作实际上包含了两步,第一步是生成正在通道中的这个元素值的副本,并准备给到接收方,第二步是删除在通道中的这个元素值、
通道可能会长时间阻塞的几种情况
缓冲通道 异步
- 当缓冲通道满了时,发送操作会被阻塞。
- 当缓冲通道内没有元素时,接收操作会被阻塞。
当接收或者发送的操作被阻塞时,它们所在的 goroutine 会顺序地进入通道内部的接收/发送等待队列,所以当通道不再堵塞操作时,其通知其他goroutine的顺序总是公平的。
非缓冲通道 同步
无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行,才会继续传递。由此可见,非缓冲通道是在用同步的方式传递数据。也就是说,只有收发双方对接上了,数据才会被传递.
零值通道
对于值为nil的通道,不论它的具体类型是什么,对它的发送操作和接收操作都会永久地处于阻塞状态,所以我们一定要记得初始化通道变量值。
发送和接收操作可能会引起panic的几种情形
- 对于一个已初始化,但并未关闭的通道来说,收发操作一定不会引发 panic。但是通道一旦关闭,再对它进行发送操作,就会引发 panic。
- 如果我们试图关闭一个已经关闭了的通道,也会引发 panic。
关闭通道的正确姿势
我们一定要在发送方关闭通道(除非有特殊的保障措施),因为接收操作是可以感知到通道的关闭,并能够安全退出的。
接收放关闭通道的判定延迟性
当我们把接收表达式的结果同时赋给两个变量时,第二个变量的类型就是一定bool类型,它的值如果为false就说明通道已经关闭,并且再没有元素值可取了。
但是如果通道关闭时,里面还有元素值未被取出,那么接收表达式的第一个结果,仍会是通道中的某一个元素值,而第二个结果值一定会是true。
因此,通过接收表达式的第二个结果值,来判断通道是否关闭是可能有延时的。
代码实例
package main
import "fmt"
func main() {
ch1 := make(chan int, 3)
ch1 <- 1
ch1 <- 2
ch1 <- 3
elem1 := <-ch1
fmt.Printf("The first element received from channel ch1: %v\n",elem1)
}
Result:
The first element received from channel ch1: 1