[译] part 22: golang channels

https://juejin.im/post/5ca318e651882543db10d4ce

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

什么是channel

channel可以被认为是Goroutines通信的管道。类似于水在管道中从一端流到另一端的方式,数据可以从一端发送,可以从另一端接收。

channel的声明

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

chan T 代表类型为Tchannel

channel的零值为nilnil channel没有任何用处,因此得使用类似于make mapmake slice来定义它。

让我们写一些声明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 in playground

在第 6 行声明了var a chan int,可以看到channel的零值为nil。因此,执行if条件内的语句并定义channel。上面的程序中的a是一个int channel。该程序将输出,

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

使用make声明也是定义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

箭头相对于通道的方向指定了是发送还是接收数据。

在第 1 行中,箭头从a向指向data,因此我们从通道a读取并将值存储到变量data中。

在第 2 行中,箭头指向a,因此我们把data写入通道a

发送和接收默认是阻塞的

默认情况下,发送和接收是阻塞的。这是什么意思?当数据发送到channel时,发送方被阻塞直到其他Goroutine从该channel读取出数据。类似地,当从channel读取数据时,读取方被阻塞,直到其他Goroutine将数据写入该channel

channel的这种属性有助于Goroutines有效地进行通信,而无需使用在其他编程语言中常见的显式锁或条件变量。

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 in playgroud 这是上一个教程的代码,这里我们将使用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 in playgroud

在上面的程序中,我们在第一行创建了一个bool型的done channel。 并将其作为参数传递给hello。第 14 行我们正在从done channel接收数据。这行代码是阻塞的,这意味着在Goroutine将数据写入done channel之前将会一直阻塞。因此,上一个程序中的time.Sleep的就没有必要了,用sleep对程序而言是相当不友好。

代码行<-done表示从done channel接收数据,如果没有任何变量使用或存储该数据,这是完全合法的。

现在我们的main Goroutine被阻塞直到done channel有数据写入。 hello Goroutine接收done channel作为参数,打印Hello world goroutine然后把true写入done channel。当这个写入完成时,main Goroutine从该done channel接收数据,然后结束阻塞打印了main函数的文本。

输出,

  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 playgroud

这个程序将首先打印Main going to call hello go goroutine。然后hello Goroutine启动,打印hello go routine is going to sleep。打印完成后,hello Goroutine将休眠 4 秒钟,在此期间main Goroutine将被阻塞,因为它正在等待来自<-done的通道的数据。 4 秒后hello Goroutine苏醒,然后打印hello go routine awake and going to write to done并写入数据到channel,接着main Goroutine接收数据并打印Main received data

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 in playgroud

calcSquares函数计算各个数字的平方的和,并将其发送到squares channel。类似地,calcCubes计算各个数字的立方的和并将其发送到cubes channel

这两个函数都作为单独的Goroutines运行。每个函数都通过一个channel作为入参。main Goroutine等待来自这两个channel的数据。一旦从两个channel接收到数据,它们就存储在squarescubes中求和,然后打印最终输出。该程序将打印,

  1. Final output 1536

死锁

使用channel时要考虑的一个重要因素是死锁。如果Goroutine正在channel上发送数据,那么期待其他一些Goroutine接收数据。如果发送的数据没有被消费,程序将在运行时产生一个panic

同样,如果Goroutine正在等待从一个channel接收数据,那么其他Goroutine应该在该channel上写入数据,否则程序也会出现panic

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

Run in playgroud

在上面的程序中,创建了一个channel ch,我们用ch <-5channel发送 5。在该程序中,没有其他Goroutinech接收数据。因此,此程序将出现以下运行时错误。

  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 in playgroud

在上面的程序中,我们在第 10 行中创建了仅发送channel sendchchan < - int表示当箭头指向chan时仅为发送channel。我们在第 12 行中尝试从该channel接收数据。 发现这是不允许的,当程序编译时,编译器会报错,

  1. main.go:11: invalid operation: <-sendch (receive from send-only type chan<- int)

看起来好像没啥问题,但是一个写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 in playgroud

在上面的程序第 10 行,创建了双向channel chnl。在第 11 行,它作为参数传递给sendData Goroutine,而sendData函数在第 5 行用sendch chan < - int将此chnl转换为仅发送的channel类型。所以现在通道只在sendData Goroutine中是单向的,但它在main Goroutine中是双向的。该程序将打印 10 作为输出。(译者注:这就是单向channel的用途,定义函数或者方法的时候,使用只读或只写会让代码更健壮。)

关闭channel和循环channel

发送者能够关闭channel以通知接收者不再在该channel上发送数据。

接收者可以在从channel接收数据时使用额外的变量来检查channel是否已关闭。

  1. v, ok := <- ch

在上面的语句中,如果成功地从该操作中接收到该值,则oktrue。如果okfalse,则表示我们正在从一个关闭的channel中读取。从关闭的channel中读取的值将是通道类型的零值。例如,如果是int类型,则从关闭的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 in playgroud

在上面的程序中,生产者Goroutine将 0 到 9 写入channel chnl,然后关闭它。在第 16 行main函数有一个无限for循环,它使变量ok检查channel是否被关闭。如果okfalse,则表示已关闭,因此循环中断。否则,将打印收到的值和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接收值,直到它被关闭。

让我们使用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 in playgroud

for range循环在第 16 行接收来自channel ch的数据直到它被关闭。 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

我们来重写一下上面那个求平方立方和的程序,

如果仔细查看程序,可以注意到在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 in playgroud

上面程序中的digits函数现在包含从number中获取各位的逻辑,并且它同时由calcSquarescalcCubes函数调用。一旦number中没有更多的位,channel就会在第 13 行被关闭。 calcSquarescalcCubes Goroutines使用for range循环监听各自的channel,直到它关闭。该程序的其余部分和之前的例子是相同的。该程序也会打印

  1. Final output 1536

该节教程就结束了,channel中还有更多的概念,例如缓冲channelworker poolselect。我们将在下一个教程中讨论它们。

ft_authoradmin  ft_create_time2019-08-03 16:37
 ft_update_time2019-08-03 16:38