美文网首页
Go教程第十九篇: Channels

Go教程第十九篇: Channels

作者: 大风过岗 | 来源:发表于2020-05-09 11:50 被阅读0次

Go教程第十九篇: Channels

本文是《Go系列教程》的第十九篇文章。

在上一篇文章中,我们讨论了并发是如何使用Goroutine实现的。在本文中,我们将详细讨论channel的有关内容,以及Goroutine是如何利用channel完成通信的。

什么是Channel ?

你可以把channel看成是管道,利用channel,goroutine完成彼此通信。和管道中水流从一端留到另一端类似,使用channel,你可以从一端发出数据,而在另一端完成数据的接收。

声明Channel

每一个channel都有一个相关的类型。此类型即是channel所允许传输的数据类型,除此之外的其他数据类型不允许使用此channel传输。

chan T 即: 数据类型为T的channel。

channel的零值是nil。nil值的channel毫无用处,因此,类似于map和slice,channel必须使用make进行定义。

我们来写段代码声明一个channel。

package main

import "fmt"

func main() {
    var a chan int
    if a == nil {
        fmt.Println("channel a is nil, going to define it")
        a = make(chan int)
        fmt.Printf("Type of a is %T", a)
    }
}

var a chan int我们在这行代码中声明的channel a是一个零值的channel,它的值为nil。因此,if条件中的语句会执行,channel a会被定义出来。程序中的a是一个int 类型的channel。程序的输出如下:

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

和往常一样,我们也可以使用快捷的语法来声明,声明channel的简洁方式是:

a := make(chan int)

上面的这行代码,也是定义了一个int类型的channel a。

从channel中发送和接收数据

从channel中发送和接收数据的语法如下:

data := <- a // read from channel a
a <- data // write to channel a

上面的代码中,和channel相关的箭头的方向,就规定了数据是发送给channel还是从channel进行数据接收。

第一行代码中,箭头是由a发出的,因此,我们是从channel a中读取数据,并把读取的数据存储到变量data中。在第二行代码中,箭头指向了a,因此,我们是在把数据写入到channel a中。

默认情况下,发送和接收都是阻塞的

channel的收发操作默认都是阻塞式的,什么意思呢? 当把数据发送给channel的时候,程序会一直阻塞在发送语句那儿,直到有其他Goroutine从该channel中执行读取操作。同样类似地,当从channel中读取数据的时候也是一样,读取操作会被一直阻塞住,直到有其他Goroutine向该channel中写入数据。

channel的这个特性可以在不使用显示的锁或条件变量的情况下,有效地解决Goroutine的通信问题。

Channel使用案例

好了,到现在为止,我们已经将了足够的理论,现在让我们写段程序理解下,Goroutine是如何使用channel完成通信的。
我们把上一节的程序拿过来,改写一下。

package main

import (
  "fmt"
  "time"
)

func hello() {
  fmt.Println("Hello world goroutine")
}
func main() {
  go hello()
  time.Sleep(1 * time.Second)
  fmt.Println("main function")
}

现在,我们使用channel改写上面的程序:

package main

import (
    "fmt"
)

func hello(done chan bool) {
    fmt.Println("Hello world goroutine")
    done <- true
}
func main() {
    done := make(chan bool)
    go hello(done)
    <-done
    fmt.Println("main function")
}

在上面的程序中,我们创建了一个bool类型的channel done,并把它作为参数传递给hello Goroutine。在第14行,我们从done channel中接收数据,这行代码会被阻塞住,即:直到有其他Goroutine向该channel中写入数据。其他Goroutine向done channel中写入数据时,程序才会往下执行。因此,这样就消除了time.sleep的使用。

<-done在这行代码中,我们从done channel中读取数据,但是并不把读取的数据存储到任何变量中,这样也是完全合法的。

现在,我们的main Goroutine会在done channel上阻塞住,一直等待来自done的数据。hello Goroutine把此channel作为参数接收,打印了“Hello world goroutine”。之后,向done channel中写入数据,当写入完成之后,main Goroutine就接收到了来自done channel的数据,它就会解除阻塞,继续往下执行。

程序的输出如下:

Hello world goroutine
main function

为了更好地理解阻塞概念,我们修改下这个程序,我们在hello Goroutine中引入sleep。

package main

import (
    "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)
    <-done
    fmt.Println("Main received data")
}

在上面的程序中,我们在hello函数中调用了sleep方法,睡眠了4秒。

程序会先输出Main going to call hello go goroutine,之后,hello Goroutine被启动,输出:hello go routine is going to sleep。在这完成之后,hello Goroutine会在done睡眠4秒,在此期间,main Goroutine将被阻塞住,因为它在调用<-done之后,就会一直等待来自done channel的数据。4秒之后,紧接着:Main received data 之后,将会输出:hello go routine awake and going to write to done。

另一个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 main

import (
    "fmt"
)

func calcSquares(number int, squareop chan int) {
    sum := 0
    for number != 0 {
        digit := number % 10
        sum += digit * digit
        number /= 10
    }
    squareop <- sum
}

func calcCubes(number int, cubeop chan int) {
    sum := 0
    for number != 0 {
        digit := number % 10
        sum += digit * digit * digit
        number /= 10
    }
    cubeop <- sum
}

func main() {
    number := 589
    sqrch := make(chan int)
    cubech := make(chan int)
    go calcSquares(number, sqrch)
    go calcCubes(number, cubech)
    squares, cubes := <-sqrch, <-cubech
    fmt.Println("Final output", squares + cubes)
}

第七行的calcSquares函数会计算给定数字上的每一个数位的平方和,并把计算结果发送给channel squareop。类似地, calcCubes 函数会计算每一个数位上数字的立方和,并把计算结果发送给cubeop通道。

这俩个函数都会运行在单独的Goroutine里面,这俩个函数都接收了一个channel作为参数。main函数的Goroutine会等待从这俩个channel中获取数据,一旦从这俩个channel中收到数据之后,它们就会被存储在squares和cubes变量中,程序的最终输出如下:

Final output 1536

死锁

我们在使用channel时,需要考虑到的一个重要情况就是死锁问题。如果一个Goroutine正在向通道中发送数据,之后,它就会等待着其他Goroutine接收此数据的。如果没有其他Goroutine从channel中获取数据,程序就会在运行时发生死锁问题。

类似地,如果一个Goroutine等待着从channel中接收数据的话,而此时就必须得有其他Goroutine向此channel中写入数据。否则的话,程序就会出错。

package main


func main() {
    ch := make(chan int)
    ch <- 5
}

在上面的程序中,会创建一个名为ch的channel。之后,我们把5发送给此通道。但是在此程序中,没有其他Goroutine从名为ch的channel中获取数据。因此程序就会在运行时发生错误。

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
   /tmp/sandbox249677995/main.go:6 +0x80

单向通道

我们上面讨论的channel都是双向的channel。双向channel我们不仅可以向channel中发送数据,同时还可以从channel中接收数据。除了双向channel之外,我们还可以创建单向channel,即只能向channel中发送数据或接收数据。

package main

import "fmt"

func sendData(sendch chan<- int) {
   sendch <- 10
}

func main() {
   sendch := make(chan<- int)
   go sendData(sendch)
   fmt.Println(<-sendch)
}

在上面的程序中,我们创建了一个只能发送数据的channel,名为sendch。chan<- int表明了这是一个单向发送channel,箭头指向chan。在紧接着的一行,我们试图从一个单向发送channel中
获取数据,这是不允许的,当我们运行程序的时候,编译器会提示:main.go:11: invalid operation: <-sendch (receive from send-only type chan<- int)。

好吧,既然如此,那么如果channel不能读取只能发送,这种单向发送通道的意义是什么呢?

这时候,就需要用到channel转换了。你可以把一个双向的channel转换成一个单向只读或单向只写的channel。但是反过来却不行。

package main

import "fmt"

func sendData(sendch chan<- int) {
    sendch <- 10
}

func main() {
    chnl := make(chan int)
    go sendData(chnl)
    fmt.Println(<-chnl)
}

在程序的第10行,我们创建了一个双向的channel。然后我们把它作为参数传递给sendData的Goroutine。此时sendData函数会把此channel转换成一个单向只读channel。
因此,在sendData的Goroutine内部,此channel只是一个单向只读channel。但是在main函数的Goroutine里面,它确是一个双向channel。程序会打印出10作为输出。

关闭通道和for循环遍历通道

发送方可以主动地关闭通道去通知接收者此channel中不再发送数据。在从channel中获取数据的时候,接收者可以使用额外的变量来判断此channel是否已经关闭。

 v, ok := <- ch

在上面的程序中,如果我们接收的是成功发送到channel中的数据的话,OK的值为true。如果OK的值是false,那也就意味着我们正在从一个关闭的通道中读取数据。从一个已关闭的channel中读取到的值为此channel类型的零值。例如,如果channel是int类型的channel的话,那么从一个已关闭的int类型的channel中读取到的值就是0。

package main

import (
    "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 := <-ch
        if ok == false {
            break
        }
        fmt.Println("Received ", v, ok)
    }
}

在上面的程序中,producer的Goroutine会向chnl通道中写入0-9,之后关闭此通道。main函数里面的for循环会检查通道是否已经关闭,如果已经关闭的话,就跳出循环,否则的话,就把
接收到的值打印出来。

程序的输出如下:

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

在上面的程序中,我们可以使用for循环重写,以提高代码复用性。

如果你仔细观察程序的话,你会看到查找一个数的每一个数位上的代码的函数在calcSquares 和calcCubes 函数中都有。其实,我们可以把这段代码封装到一个单独的函数中,从而可以并发地调用它。

package main

import (
    "fmt"
)

func digits(number int, dchnl chan int) {
    for number != 0 {
        digit := number % 10
        dchnl <- digit
        number /= 10
    }
    close(dchnl)
}
func calcSquares(number int, squareop chan int) {
    sum := 0
    dch := make(chan int)
    go digits(number, dch)
    for digit := range dch {
        sum += digit * digit
    }
    squareop <- sum
}

func calcCubes(number int, cubeop chan int) {
    sum := 0
    dch := make(chan int)
    go digits(number, dch)
    for digit := range dch {
        sum += digit * digit * digit
    }
    cubeop <- sum
}

func main() {
    number := 589
    sqrch := make(chan int)
    cubech := make(chan int)
    go calcSquares(number, sqrch)
    go calcCubes(number, cubech)
    squares, cubes := <-sqrch, <-cubech
    fmt.Println("Final output", squares+cubes)
}

digits函数封装了从一个数字中提取每个数位上数字的代码。它被calcSquares 和 calcCubes函数并发地调用。一旦此数字上没有更多的数位时,通道就会关闭掉。
calcSquares 和 calcCubes的Goroutine会使用for range 循环监听他们各自的channel,直到通道关闭。程序的剩下部分基本上都一样。程序将输出如下:

Final output 1536  

感谢您的阅读,请留下您珍贵的反馈和评论。Have a good Day!

备注
本文系翻译之作原文博客地址

相关文章

网友评论

      本文标题:Go教程第十九篇: Channels

      本文链接:https://www.haomeiwen.com/subject/ahzhnhtx.html