在上一篇教程中,我们讨论了如何使用 Goroutines 在 Go 中实现并发。在本教程中,我们将讨论 channel 以及 Goroutines 如何使用 channel 进行通信。
什么是 Channel
channel 可以看作是管道,Goroutines 使用管道进行通信。和水在管道中从一端流向另一端一样,数据可以通过管道从一端发送,从另一端接收。
声明 Channel
每个 channel 都有一个与之关联的类型。这种类型是 channel 允许传输的数据类型。不允许使用 channel 传输其他类型。
chan T 是 T 类型的 channel 
channel 的零值为 nil。nil  channel 没有任何作用,因此必须使用 make 来定义 channel ,跟定义 maps 和 slices 类似。
让我们编写声明 channel 的代码。
package mainimport "fmt"func main() {var a chan intif a == nil {fmt.Println("channel a is nil, going to define it")a = make(chan int)fmt.Printf("Type of a is %T", a)}}
在第 6 行中声明的 nil channel  a。因此,执行 if 条件中的语句并定义 channel 。上面程序中的 a 是一个 int  channel 。这个程序将输出 
channel a is nil, going to define itType of a is chan int
通常,简短的声明也是定义 channel 有效和简洁的方法。
a := make(chan int)
上面的代码行也定义了一个int channel  a。
Channel 发送和接收
下面给出了从 channel 发送和接收数据的语法
data := <- a // read from channel aa <- data // write to channel a
箭头相对于 channel 的方向指定数据是发送还是接收。
在第一行中,箭头从 a 指向内,因此我们从 channel  a 读取数据并将值存储到变量 data。
在第二行,箭头指向 a,因此我们向 channel  a 写数据。
发送和接收在默认情况下是阻塞的
默认情况下,对 channel 的发送和接收是阻塞的。这是什么意思呢?当数据被发送到一个 channel 时,send 语句中被阻塞,直到其他 Goroutine 从该 channel 读取数据为止。类似地,当从 channel 读取数据时,读取将被阻塞,直到 Goroutine 将数据写入该 Channel 。
channel 的这一特性有助于 goroutine 在不使用显式锁或条件变量的情况下有效地通信,而显式锁或条件变量在其他编程语言中非常常见。
Channel 示例程序
前面讲了太多的理论:)。让我们编写一个程序来理解 Goroutine 如何使用 channel 进行通信。
我们将重写我们在学习 Goroutines 使用 channel 时编写的程序。
让我引用上一篇教程中的程序。
package mainimport ("fmt""time")func hello() {fmt.Println("Hello world goroutine")}func main() {go hello()time.Sleep(1 * time.Second)fmt.Println("main function")}
这是上节课的程序。我们在这里使用 sleep 让主程序 Goroutine 等待 Goroutine hello 完成。如果你不理解这一点,我建议你阅读 Goroutines 的教程
我们将使用 channel 重写上面的程序。
package mainimport ("fmt")func hello(done chan bool) {fmt.Println("Hello world goroutine")done <- true}func main() {done := make(chan bool)go hello(done)<-donefmt.Println("main function")}
在上面的程序中,代码第 13 行我们创建了一个 done bool channel 。 并将其作为参数传递给 hello Goroutine。我们正在从 done  channel 接收数据。这行代码是阻塞的,这意味着在 Goroutine 将数据写入 done  channel 之前,程序不会执行下一行代码。因此,我们就不需要原始程序中的 time.Sleep 了。
代码行 <-done 从 done  channel 接收数据,但不在任何变量中使用或存储该数据。这是完全合法的。
现在我们的 main Goroutine 被阻塞等待 done  channel 的数据。 hello  Goroutine接收此 channel 作为参数,输出 Hello world  goroutine 然后写入 done  channel 。当这个写入完成时,main Goroutine 从 done  channel 接收数据,它被解锁,然后输出 main function。
程序输出
Hello world goroutinemain function
让我们通过在 hello Goroutine中引入 sleep 来修改这个程序,以更好地理解这个阻塞概念。
package mainimport ("fmt""time")func hello(done chan bool) {fmt.Println("hello go routine is going to sleep")time.Sleep(4 * time.Second)fmt.Println("hello go routine awake and going to write to done")done <- true}func main() {done := make(chan bool)fmt.Println("Main going to call hello go goroutine")go hello(done)<-donefmt.Println("Main received data")}
在上面的程序第 10 行中,我们为第一行中的 hello 函数添加了 4 秒睡眠。
这个程序将首先输出 Main going to call hello go goroutine。然后 hello Goroutine 将被启动,它将输出 hello go routine is going to sleep。输出完后,hello Goroutine 将休眠 4 秒,在此期间,main Goroutine 将被阻塞,因为它在 <-done 中等待来自 done  channel 的数据。4 秒后,输出 hello go routine awake and going to write to done,接着输出 Main received data。
Channel 的其他例子
让我们再写一个程序来更好地理解 channel 。该程序将输出组成一个数字的各个数字的平方和和立方和的总和。
例如,如果 123 是输入,那么这个程序将输出为
squares = (1 1) + (2 2) + (3 * 3)
cubes = (1 1 1) + (2 2 2) + (3 3 3)
output = squares + cubes = 50
我们将把程序结构成这样:在一个单独的 Goroutine 中计算正方形,在另一个Goroutine 中计算立方体,最后的求和在 main Goroutine 中进行。
package mainimport ("fmt")func calcSquares(number int, squareop chan int) {sum := 0for number != 0 {digit := number % 10sum += digit * digitnumber /= 10}squareop <- sum}func calcCubes(number int, cubeop chan int) {sum := 0for number != 0 {digit := number % 10sum += digit * digit * digitnumber /= 10}cubeop <- sum}func main() {number := 589sqrch := make(chan int)cubech := make(chan int)go calcSquares(number, sqrch)go calcCubes(number, cubech)squares, cubes := <-sqrch, <-cubechfmt.Println("Final output", squares + cubes)}
calcSquares 函数在第 7 行中计算组成一个数字的各个数的平方和,并将其发送到squareop  channel 。类似地,calcCubes 在第 17 中计算数字的各个数字的立方之和并将其发送到 cubeop  channel 。
这两个函数在第一行中作为单独的 Goroutines 运行。在第 31 和 32 行中,每个都输入一个 channel 作为参数。main Goroutine 等待来自这两个 channel 的数据。  第 33 行中一旦从两个 channel 接收到数据,它们就存储在 squares 和 cubes 变量中。最终输出。该程序将输出
Final output 1536
死锁
使用 channel 时要考虑的一个重要因素是死锁。如果 Goroutine 在 channel 上发送数据,那么其他 Goroutine 应该接收数据。如果没有发生这种情况,那么程序将在运行时陷入死锁。
类似地,如果一个 Goroutine 正在等待从一个 channel 接收数据,那么另一个 Goroutine 将在该 channel 上写入数据,否则程序将陷入异常。
package mainfunc main() {ch := make(chan int)ch <- 5}
在上面的程序中,创建了一个 channel  ch,我们将 5 发送到 ch <- 5。在这个程序中,没有其他 Goroutine 从 channel  ch 接收数据。
fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan send]:main.main()/tmp/sandbox249677995/main.go:6 +0x80
单向 Channel
到目前为止,我们讨论的所有 channel 都是双向 channel ,即数据可以在它们上面发送和接收。还可以创建单向 channel ,即只发送或接收数据的 channel 。
package mainimport "fmt"func sendData(sendch chan<- int) {sendch <- 10}func main() {sendch := make(chan<- int)go sendData(sendch)fmt.Println(<-sendch)}
在上面的程序中,我们在第 10 行中创建了只能发送数据的 channel  sendch 。当箭头指向chan 时,chan<- int 表示只有发送数据的 channel 。我们试着从第 12 行只发送的 channel 中接收数据,这是不允许的,当程序运行时,编译器会报错
main.go:11: invalid operation: <-sendch (receive from send-only type chan<- int)
但是如果无法从发送 channel 读取数据,那么只向发送 channel 写入数据又有什么意义呢!
这就是使用 channel 转换的地方。可以将双向 channel 转换为仅发送或仅接收 channel ,但反之则不行。
**
package mainimport "fmt"func sendData(sendch chan<- int) {sendch <- 10}func main() {chnl := make(chan int)go sendData(chnl)fmt.Println(<-chnl)}
上面的程序第 10 行中,创建了双向 channel  chnl 。它作为参数传递给第 11 行的 sendData Goroutine。 sendData 函数将此 channel 转换为第 5 行中的仅发送数据 channel  sendch chan<- int。所以现在 channel 只在 sendData Goroutine中发送,但它在 main Goroutine 中是双向的。该程序将输出 10。
关闭 Channel 和 Channel 循环
发送方能够关闭 channel ,通知接收方 channel 上不再发送数据。
接收方可以在接收来自 channel 的数据时使用附加变量来检查 channel 是否已关闭。
v, ok := <- ch
在上面的语句中,如果通过成功的发送操作接收到某个 channel 的值,ok 为 true。如果 ok 为 false,则表示我们从一个已经关闭的 channel 读取值。从关闭的 channel 读取的值是 channel 类型的零值。例如,如果 channel 是 int 类型的 channel ,那么从关闭的 channel 接收的值将为 0。
package mainimport ("fmt")func producer(chnl chan int) {for i := 0; i < 10; i++ {chnl <- i}close(chnl)}func main() {ch := make(chan int)go producer(ch)for {v, ok := <-chif ok == false {break}fmt.Println("Received ", v, ok)}}
在上面的程序中,producer Goroutine 将 0 到 9 写入 chnl  channel ,然后关闭 Channel 。主函数在第 16 行中是一个无限 for 循环,它使用第 18 行中的变量 ok 检查 channel 是否关闭。如果 ok 为 false,则表示 channel 已关闭,因此循环中断。否则,将输出收到的值和 ok 的值。这个程序输出,
Received 0 trueReceived 1 trueReceived 2 trueReceived 3 trueReceived 4 trueReceived 5 trueReceived 6 trueReceived 7 trueReceived 8 trueReceived 9 true
for 循环的 for range 形式可用于从 channel 接收值,直到 channel 关闭。
让我们使用 for range 循环重写上面的程序。
package mainimport ("fmt")func producer(chnl chan int) {for i := 0; i < 10; i++ {chnl <- i}close(chnl)}func main() {ch := make(chan int)go producer(ch)for v := range ch {fmt.Println("Received ",v)}}
16 行的 for range 循环从 ch  channel 接收数据,直到它被关闭。一旦 ch 关闭,循环将自动退出。这个程序输出
Received 0Received 1Received 2Received 3Received 4Received 5Received 6Received 7Received 8Received 9
可以使用 for range 循环重写另外一个channels 的程序,使其具有更多的代码重用性。
如果你仔细查看这个程序会注意到,在 calcSquares 函数和 calcCubes 函数中都重复了找到数字的单个数的代码。我们将把代码封装到到它自己的函数中并同时调用它。
package mainimport ("fmt")func digits(number int, dchnl chan int) {for number != 0 {digit := number % 10dchnl <- digitnumber /= 10}close(dchnl)}func calcSquares(number int, squareop chan int) {sum := 0dch := make(chan int)go digits(number, dch)for digit := range dch {sum += digit * digit}squareop <- sum}func calcCubes(number int, cubeop chan int) {sum := 0dch := make(chan int)go digits(number, dch)for digit := range dch {sum += digit * digit * digit}cubeop <- sum}func main() {number := 589sqrch := make(chan int)cubech := make(chan int)go calcSquares(number, sqrch)go calcCubes(number, cubech)squares, cubes := <-sqrch, <-cubechfmt.Println("Final output", squares+cubes)}
上面程序中的 digits 函数现在包含了从一个数字中获取单个数字的逻辑,它由calcSquares 函数和 calcCubes 函数同时调用。当数字中没有更多的数字时, channel 在第 13 行中关闭。calcSquares 和 calcCubes Goroutines 使用 for range 循环监听它们各自的 channel ,直到它关闭为止。程序其余都是相同的。这个程序会输出
Final output 1536
这就是本教程的结尾。 channel 中还有其他概念,比如缓冲 channel 、工作池和 select。我们将在单独的教程中讨论它们。
