如果说 goroutine 是 Go 程序并发的执行体,通道就是它们之间的连接。通道是可以让一个goroutine 发送特定值到另一个 goroutine 的通信机制。每一个通道是一个具体类型的导管,叫做通道的元素类型。一个有 int 类型元素的通道写为 chan int。
ch := make(chan int) // ch 的类型是 'chan int'
像 map 一样,通道是一个使用 make 创建的数据结构的引用。当复制或者作为参数传递到一个函数时,复制的是引用,这样调用者和被调用者都引用同一份数据结构。和其他引用类型一样,通道的零值是 nil。
同种类型的通道可以用 == 符号就行比较。当二者都是同一通道数据的引用时,比较值为 true 。通道可以和 nil 进行比较。
通道有两个主要操作:发送(send)和接收 (receive),两者统称为通信。send 语句从一个goroutine 传输一个值到另一个在执行接收表达式的 goroutine。
ch <- x // 发送语句
x = <-ch // 赋值语句中的接收表达式
<-ch // 接收语句,丢弃结果
通道支持第三个操作:关闭(close),它设置一个标志位来指示值当前已经发送完毕,这个通道后面没有值了;关闭后的发送操作将导致宕机。在一个已经关闭的通道上进行接收操作,将获取所有已经发送的值,直到通道为空;这时任何接收操作会立即完成,同时获取到一个通道元素类型对应的零值。
无缓冲通道
无缓冲通道上的发送操作将会阻塞,直到另一个 goroutine 在对应的通道上执行接收操作,这时值传送完成,两个 goroutine 都可以继续执行。相反,如果接收操作先执行,接收方 goroutine 将阻塞,直到另一个 goroutine 在同一个通道上发送一个值。
使用无缓冲通道进行通信导致发送和接收 goroutine 同步化。因此,无缓冲通道也称为同步通道。
管道
通道可以用来连接 goroutine,这样一个输出是另一个的输入。这个叫管道。
在通道关闭后,任何后续的发送操作将会导致应用崩溃。当关闭的通道被读完(就是最后一个发送的值被接收)后,所有后续的接收操作顺畅进行,只是获取到的是零值。
没有一个直接的方式来判断通道是否已经关闭,但是这里有接收操作的一个变种,它产生两个结果:接收到的通道元素,以及一个布尔值(通常称为 ok),它为 true 的时候代表接收成功,false 表示当前的接收操作在一个关闭的并且读完的通道上。
// squarer
go func() {
for {
x , ok := <-naturals
if !ok {
break
}
squarer <- x * x
}
close(squarer)
}()
单向通道类型
类型 chan <- int 是一个只能发送的通道,允许发送但不允许接收。反之,类型 <-chan int 是一个只能接收的 int 类型通道,允许接收但是不能发送。(<- 操作符相当于 chan 关键字的位置是一个帮助记忆的点)。
任何赋值操作中将双向通道转换为单向通道都是允许的,但是反过来是不行的。
缓冲通道
ch := make(chan string, 3)
缓冲通道的发送操作在队列的尾部插入一个元素,接收操作从队列的头部移除一个元素。如果通道满了,发送操作会阻塞所在的 goroutine 直到另一个 goroutine 对它进行接收操作来留出可用的空间。反过来,如果通道是空的,执行接收操作的 goroutine 阻塞,直到另一个 goroutine 在通道上发送数据。
新手有时候粗暴地将缓冲通道作为队列在单个 goroutine 中使用,但是这是一个错误。通道和 goroutine 的调度深度关联,如果没有另一个 goroutine 从通道进行接收,发送者(也许是整个程序)有被永久阻塞的风险。如果仅仅需要一个简单的队列,使用 slice 创建一个就可以了。
使用 select 多路复用
有时候不能只从一个通道上接收,因为哪一个操作都会在完成前阻塞。所以需要多路复用的操作,需要一个 select 语句:
select {
case <-ch1:
// ....
case x :=<-ch2:
// .....
case ch3 <- y:
// ....
default:
// ....
}
以上是 select 语句的通用形式。像 switch 语句一样,它有一系列的情况和一个可选的默认分支。每一个情况都指定一次通信(在一些通道上进行发送或接收操作)和关联的一段代码块。接收表达式操作可能出现在它本身上,像第一个情况,或者在一个短变量声明中,像第二个情况;第二种形式可以让你引用所接收的值。
select 一直等待,直到一次通信来告知一些情况可以执行。然后,它进行这次通信,执行此情况所对应的语句;其他的通信将不会发生。对于没有对应情况的 select,select{} 将永远等待。
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case x := <-ch:
fmt.Println(x) // 0 2 4 6 8
case ch <- i:
}
}
如果多个情况同时满足,select 会随机选择一个,这样保证每一个通道有相同的机会被选中。