在上一篇教程中,我们讨论了如何使用 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 的代码。

  1. package main
  2. import "fmt"
  3. func main() {
  4. var a chan int
  5. if a == nil {
  6. fmt.Println("channel a is nil, going to define it")
  7. a = make(chan int)
  8. fmt.Printf("Type of a is %T", a)
  9. }
  10. }

Run program in playground

在第 6 行中声明的 nil channel a。因此,执行 if 条件中的语句并定义 channel 。上面程序中的 a 是一个 int channel 。这个程序将输出


  1. channel a is nil, going to define it
  2. Type of a is chan int

通常,简短的声明也是定义 channel 有效和简洁的方法。

  1. a := make(chan int)

上面的代码行也定义了一个int channel a。

Channel 发送和接收

下面给出了从 channel 发送和接收数据的语法

  1. data := <- a // read from channel a
  2. a <- 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 时编写的程序。

让我引用上一篇教程中的程序。

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func hello() {
  7. fmt.Println("Hello world goroutine")
  8. }
  9. func main() {
  10. go hello()
  11. time.Sleep(1 * time.Second)
  12. fmt.Println("main function")
  13. }

Run program in playground

这是上节课的程序。我们在这里使用 sleep 让主程序 Goroutine 等待 Goroutine hello 完成。如果你不理解这一点,我建议你阅读 Goroutines 的教程

我们将使用 channel 重写上面的程序。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func hello(done chan bool) {
  6. fmt.Println("Hello world goroutine")
  7. done <- true
  8. }
  9. func main() {
  10. done := make(chan bool)
  11. go hello(done)
  12. <-done
  13. fmt.Println("main function")
  14. }

Run program in playground

在上面的程序中,代码第 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。

程序输出


  1. Hello world goroutine
  2. main function

让我们通过在 hello Goroutine中引入 sleep 来修改这个程序,以更好地理解这个阻塞概念。

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func hello(done chan bool) {
  7. fmt.Println("hello go routine is going to sleep")
  8. time.Sleep(4 * time.Second)
  9. fmt.Println("hello go routine awake and going to write to done")
  10. done <- true
  11. }
  12. func main() {
  13. done := make(chan bool)
  14. fmt.Println("Main going to call hello go goroutine")
  15. go hello(done)
  16. <-done
  17. fmt.Println("Main received data")
  18. }

Run in playground

在上面的程序第 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 中进行。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func calcSquares(number int, squareop chan int) {
  6. sum := 0
  7. for number != 0 {
  8. digit := number % 10
  9. sum += digit * digit
  10. number /= 10
  11. }
  12. squareop <- sum
  13. }
  14. func calcCubes(number int, cubeop chan int) {
  15. sum := 0
  16. for number != 0 {
  17. digit := number % 10
  18. sum += digit * digit * digit
  19. number /= 10
  20. }
  21. cubeop <- sum
  22. }
  23. func main() {
  24. number := 589
  25. sqrch := make(chan int)
  26. cubech := make(chan int)
  27. go calcSquares(number, sqrch)
  28. go calcCubes(number, cubech)
  29. squares, cubes := <-sqrch, <-cubech
  30. fmt.Println("Final output", squares + cubes)
  31. }

Run program in playground

calcSquares 函数在第 7 行中计算组成一个数字的各个数的平方和,并将其发送到squareop channel 。类似地,calcCubes 在第 17 中计算数字的各个数字的立方之和并将其发送到 cubeop channel 。

这两个函数在第一行中作为单独的 Goroutines 运行。在第 31 和 32 行中,每个都输入一个 channel 作为参数。main Goroutine 等待来自这两个 channel 的数据。 第 33 行中一旦从两个 channel 接收到数据,它们就存储在 squares 和 cubes 变量中。最终输出。该程序将输出

  1. Final output 1536

死锁

使用 channel 时要考虑的一个重要因素是死锁。如果 Goroutine 在 channel 上发送数据,那么其他 Goroutine 应该接收数据。如果没有发生这种情况,那么程序将在运行时陷入死锁。

类似地,如果一个 Goroutine 正在等待从一个 channel 接收数据,那么另一个 Goroutine 将在该 channel 上写入数据,否则程序将陷入异常。

  1. package main
  2. func main() {
  3. ch := make(chan int)
  4. ch <- 5
  5. }

Run program in playground

在上面的程序中,创建了一个 channel ch,我们将 5 发送到 ch <- 5。在这个程序中,没有其他 Goroutine 从 channel ch 接收数据。

  1. fatal error: all goroutines are asleep - deadlock!
  2. goroutine 1 [chan send]:
  3. main.main()
  4. /tmp/sandbox249677995/main.go:6 +0x80

单向 Channel

到目前为止,我们讨论的所有 channel 都是双向 channel ,即数据可以在它们上面发送和接收。还可以创建单向 channel ,即只发送或接收数据的 channel 。

  1. package main
  2. import "fmt"
  3. func sendData(sendch chan<- int) {
  4. sendch <- 10
  5. }
  6. func main() {
  7. sendch := make(chan<- int)
  8. go sendData(sendch)
  9. fmt.Println(<-sendch)
  10. }

Run program in playground

在上面的程序中,我们在第 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 ,但反之则不行。

**

  1. package main
  2. import "fmt"
  3. func sendData(sendch chan<- int) {
  4. sendch <- 10
  5. }
  6. func main() {
  7. chnl := make(chan int)
  8. go sendData(chnl)
  9. fmt.Println(<-chnl)
  10. }

Run program in playground

上面的程序第 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 是否已关闭。

  1. v, ok := <- ch

在上面的语句中,如果通过成功的发送操作接收到某个 channel 的值,ok 为 true。如果 ok 为 false,则表示我们从一个已经关闭的 channel 读取值。从关闭的 channel 读取的值是 channel 类型的零值。例如,如果 channel 是 int 类型的 channel ,那么从关闭的 channel 接收的值将为 0。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func producer(chnl chan int) {
  6. for i := 0; i < 10; i++ {
  7. chnl <- i
  8. }
  9. close(chnl)
  10. }
  11. func main() {
  12. ch := make(chan int)
  13. go producer(ch)
  14. for {
  15. v, ok := <-ch
  16. if ok == false {
  17. break
  18. }
  19. fmt.Println("Received ", v, ok)
  20. }
  21. }

Run program in playground

在上面的程序中,producer Goroutine 将 0 到 9 写入 chnl channel ,然后关闭 Channel 。主函数在第 16 行中是一个无限 for 循环,它使用第 18 行中的变量 ok 检查 channel 是否关闭。如果 ok 为 false,则表示 channel 已关闭,因此循环中断。否则,将输出收到的值和 ok 的值。这个程序输出,

  1. Received 0 true
  2. Received 1 true
  3. Received 2 true
  4. Received 3 true
  5. Received 4 true
  6. Received 5 true
  7. Received 6 true
  8. Received 7 true
  9. Received 8 true
  10. Received 9 true

for 循环的 for range 形式可用于从 channel 接收值,直到 channel 关闭。

让我们使用 for range 循环重写上面的程序。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func producer(chnl chan int) {
  6. for i := 0; i < 10; i++ {
  7. chnl <- i
  8. }
  9. close(chnl)
  10. }
  11. func main() {
  12. ch := make(chan int)
  13. go producer(ch)
  14. for v := range ch {
  15. fmt.Println("Received ",v)
  16. }
  17. }

Run program in playground

16 行的 for range 循环从 ch channel 接收数据,直到它被关闭。一旦 ch 关闭,循环将自动退出。这个程序输出

  1. Received 0
  2. Received 1
  3. Received 2
  4. Received 3
  5. Received 4
  6. Received 5
  7. Received 6
  8. Received 7
  9. Received 8
  10. Received 9

可以使用 for range 循环重写另外一个channels 的程序,使其具有更多的代码重用性。

如果你仔细查看这个程序会注意到,在 calcSquares 函数和 calcCubes 函数中都重复了找到数字的单个数的代码。我们将把代码封装到到它自己的函数中并同时调用它。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func digits(number int, dchnl chan int) {
  6. for number != 0 {
  7. digit := number % 10
  8. dchnl <- digit
  9. number /= 10
  10. }
  11. close(dchnl)
  12. }
  13. func calcSquares(number int, squareop chan int) {
  14. sum := 0
  15. dch := make(chan int)
  16. go digits(number, dch)
  17. for digit := range dch {
  18. sum += digit * digit
  19. }
  20. squareop <- sum
  21. }
  22. func calcCubes(number int, cubeop chan int) {
  23. sum := 0
  24. dch := make(chan int)
  25. go digits(number, dch)
  26. for digit := range dch {
  27. sum += digit * digit * digit
  28. }
  29. cubeop <- sum
  30. }
  31. func main() {
  32. number := 589
  33. sqrch := make(chan int)
  34. cubech := make(chan int)
  35. go calcSquares(number, sqrch)
  36. go calcCubes(number, cubech)
  37. squares, cubes := <-sqrch, <-cubech
  38. fmt.Println("Final output", squares+cubes)
  39. }

Run program in playground

上面程序中的 digits 函数现在包含了从一个数字中获取单个数字的逻辑,它由calcSquares 函数和 calcCubes 函数同时调用。当数字中没有更多的数字时, channel 在第 13 行中关闭。calcSquares 和 calcCubes Goroutines 使用 for range 循环监听它们各自的 channel ,直到它关闭为止。程序其余都是相同的。这个程序会输出

  1. Final output 1536

这就是本教程的结尾。 channel 中还有其他概念,比如缓冲 channel 、工作池和 select。我们将在单独的教程中讨论它们。

原文链接

https://golangbot.com/channels/