上一篇教程中,我们讨论了如何使用 Goroutines 在 Go 中实现并发。在本教程中,我们将讨论 channel 以及 Goroutines 如何使用 channel 进行通信。

什么是 Channel

channel 可以看作是管道,Goroutines 使用管道进行通信。和水在管道中从一端流向另一端一样,数据可以通过管道从一端发送,从另一端接收。

声明 Channel

每个 channel 都有一个与之关联的类型。这种类型是 channel 允许传输的数据类型。不允许使用 channel 传输其他类型。

chan TT 类型的 channel

channel 的零值为 nilnil 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 接收到数据,它们就存储在 squarescubes 变量中。最终输出。该程序将输出

  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 行中关闭。calcSquarescalcCubes Goroutines 使用 for range 循环监听它们各自的 channel ,直到它关闭为止。程序其余都是相同的。这个程序会输出

  1. Final output 1536

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

原文链接

https://golangbot.com/channels/