5

GO语言学习笔记-Channels

 2 years ago
source link: https://www.hi-roy.com/posts/go%E8%AF%AD%E8%A8%80%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0-channels/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

GO语言学习笔记-Channels

2018-06-04

原文,建议先看完goroutine部分再看这篇。

什么是channels

channels可以理解成是goroutine之间通信的管道,和水流从管道的一端到另一端类似,数据也可以从管道的一端发送另一端接收。

声明channels

每个channel都需指定一个类型,这个类型是表明哪种类型的数据可以通过管道传输,而其他类型的不可以。

chan T指接受类型T的channel。

channel的默认值是nilnil channel不能被任何类型使用所以和map或者slices一样,要使用make关键字来进行定义。

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)
    }
}

上面声明了变量名为a的channel,并且默认值是nil,因此判断语句成立并且初始化类型为int的channel,程序输出如下:

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

通常我们使用一种更简洁的办法:

a := make(chan int)

发送和接收数据

语法如下:

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

箭头指向的方向表明了是读取还是接收数据。第一行,箭头向外指出,所以代表从a中读取数据并赋值给data变量。第二行,箭头指向a因此是向a中写入数据。

读写默认是阻塞行为

对channel进行读写操作默认是阻塞的,什么意思呢?当向channel写入数据时,程序被阻塞在写数据的语句处,直到有其他的goroutine从channel中读取。同样的,当从channel中读数据也会阻塞直到其他goroutine向其中写数据。

这种特性帮助goroutine之间进行高效通信,而不用像其他编程语言中那样使用显示声明锁或者条件变量来实现。

说完理论,我们来编写代码看看goroutine如何使用channel进行通信。我们先复习一下上一篇学习goroutine中的代码:

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")
}

我们使用Sleep来阻塞了main goroutine等待hello goroutine执行完毕,如果你对这个不理解,请看前一篇文章

我们使用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")
}

上面程序中我们创建了名为done的布尔型channel并作为参数传递给了hello这个goroutine,14行我们从done中读取数据,这行代码将被阻塞直到其他的goroutine向其中写入数据,因此不再需要Sleep来阻止main goroutine继续执行了。

<-done这行代码从channel中读取数据但不使用任何变量存储,这是符合语法的。

现在main goroutine被阻塞,等待done中的数据,hello接收这个channel作为参数, 输出Hello world goroutine并且向done中写数据。当写入完成后,main goroutinedone中读取数据并且解除阻塞,接下来打印main function

程序输出如下:

Hello world goroutine  
main function  

再次引入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中sleep4秒钟。

程序首先会输出Main going to call hello go goroutine并且启动goroutine并输出hello go routine is going to sleep。之后hello goroutine被阻塞4秒钟,与此同时main goroutine也会被阻塞,因为它需要在done中读取数据。4秒钟后将输出ello go routine awake and going to write to doneMain received data

让我们来写一个更复杂的例子来理解channel,这个程序要求,输入一个数字,输出其每一位的平方和与立方和,并对这2者求和。比如我们输入123,则

squares = (1 * 1) + (2 * 2) + (3 * 3)

cubes = (1 * 1 * 1) + (2 * 2 * 2) + (3 * 3 * 3)

output = squares + cubes = 50

我们分别在squares goroutinecubes goroutine中进行计算,并在main goroutine中进行最后求和。(roy注:可以先自己实现再看下面的答案)

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)
    // fmt.Println("Final output ", <-sqrch+<-cubech) roy注 直接读出来也行
}

calcSquarescalcCubes进行并发计算并把结果存储到相应的channel中,main goroutine等待计算结果完成后输出:

Final output 1536

使用channel中一个非常重要的问题就是死锁,如果一个goroutine向channel中写入了数据,那么应该有其他的goroutine读取数据。如果没有,程序将报错Deadlock。类似的,如果goroutine等待从channel中读取数据,那么应该有其他的goroutine向channel中写入数据,否则程序也将报错。

package main


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

上面的代码创建了channel ch并向其中写入5,但并没有goroutine从ch中读取数据,所以程序报错:

fatal error: all goroutines are asleep - deadlock!

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

单向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。chan<- int这个符号表明只能向这个channel写入数据,12行我们尝试从这个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)
}

上面程序中我们创建了一个双向channel chnl,并把他作为参数传递给sendData,第5行函数通过sendch chan<- int将其转换为单向channel,所以在函数内部这个channel只能写入数据,而在main函数里chnl依然是个双向channel。程序将输出10

关闭channel和range循环

发送方可以关闭channel来通知接收方没有更多数据传递了,而接收方可以使用额外的变量获取channel是否被关闭。

v, ok := <- ch

上面的代码中,如果ok的值是true则代表成功从channel获取到了值,为false则代表从一个已经关闭了的channel中读取数据,读取到的值为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)
    }
}

上面的程序producerchnl中写入0-9后关闭channel,main中使用for循环来检查channel是否被关闭,如果ok值为false则代表channel被关闭并且跳出循环,否则输出读取的值和ok

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 range来读取数据直到channel被关闭:

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 := range ch {
        fmt.Println("Received ",v)
    }
}

一旦channel关闭,循环将自动结束。程序输出和上面一样。

我们可以使用for range重写上面求和的程序来提高可重用性。如果你仔细观察,你将注意到从数字中提取某一位的代码是重复的,我们将这一步提取出来:

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函数包含提取数字逻辑并且被calcSquarescalcCubes函数并发调用,一旦没有更多位数需要提取,channel将被关闭。calcSquarescalcCubes函数各自使用for range循环读取channel中的数据,直到其被关闭。其他部分是一样的,程序将输出:

Final output 1536

接下来我们还要介绍关于channel更多的概念,比如buffered channelsworker poolsselect,欢迎持续关注。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK