Part 22: 通道(channel)

欢迎来到Golang教程系列 的第22节


上一个教程,我们讨论了关于在Go中如何使用协程实现并发。在该教程,我们将讨论关于通道以及Goroutines 如何使用通道实现通信。

什么是通道(channel)

通道可以被想像为Goroutines通信使用的管道(pipe)。和水流如何从管道的一端流向另一端类似,使用通道可以使用数据从一端发送,从另一端接收。

声明通道(channel)

每个通道都有一个与它关联的类型。这个类型是允许通道传输的数据的类型。使用该通道不允许传输其他类型。

chan T 是类型为 T 的通道

通道的零值是 nilnil 通道没有任何用处,因此通道与 mapslice类似,通过使用 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)
    }
}

被声明在第 6 行的通道 a 的值是 nil,作为通道的零值是 nil 。因此在 if 条件的语句被执行,通道被定义。上面程序的 a 是个 int 类型的通道。程序将打印

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

通常,简写声明也是定义通道的简洁而正确的方法。

a := make(chan int)

上面的一行代码也是定义一个 int 通道 a

发送和从通道接收

从通道发送和接收数据的语法如下

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

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

在第一行,箭头指向 a 的外侧,因此我们从通道 a 读,将值存储在变量 data 中。

在第二行,箭头指向 a,因此我们向通道 a 写。

发送和接收默认是阻塞的

对于通道,发送和接收默认是阻塞的。这是什么意思?当数据被发送到通道,在发送语句中的控制是阻塞的,直到一些其他 Goroutine 从通道读数据。类似地,当数据从通道读取后,读操作是阻塞的,直到一些 Goroutine 向通道写数据。

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

Channel 示例程序

理论已经足够了,我们写个程序来理解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 来使主协程等等 hello 协程的完成。如果你对此没有印象,我建议阅读 Goroutine教程。

我们使用通道来重写上面的程序

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

在上面的程序,我们在 12 行创建一个 done 布尔通道,作为参数传送给 hello 协程。在 14 行,我们从 done 通道接收 数据,这行代码是阻塞的,它意味着直到一些协程向 done 协程写数据,否则控制将不会移动到下一行代码。因此这消除了对原始程序中存在的time.Sleep 的需要,以防止主要的Goroutine退出。

<-done 代码行从通道接收数据但并不在任何变量存储或使用它,这当然是合法的。

现在我们的 main 协程阻塞,等等done通道的数据。hello 协程接收作为参数的通道,打印 Hello world goroutine 然后写 done 通道。当该写操作完成,主协程从 done 协程接收数据,这是非阻塞的,然后 文本 main function 被打印。

这个程序的输出

Hello world goroutine
main function

我们通过在 hello 协程中引入 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 函数的第 10 行的引入 sleep 休眠 4 秒。

这个程序将首先打印 Main going to call hello go goroutine。然后 hello 协程将被启动并打印 hello goroutine is going to sleep。在这个打印之后,hello 协程将休眠 4 秒,在这些时间内, main 协程将被阻塞,直到在 <-done 中读取到数据。在 4 秒后 hello go routine awake and going to write to done 将被打印,然后是 Main receive data

通道的另一个例子

我们再写一个例子以更好的理解通道。这个程序将打印每个数据的平方和立方的和。

例如如果输入的是123,则程序将计算输出为

squares=(11)+(22)+(33)
cubes=(1
11)+(222)+(33*3)
output=squares + cubes = 50

我们将构建程序,squares 在一个分离的Goroutine 程序中计算,cubes 在另一个协程中,最后在 main 协程中计算和。

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

在第 7 行的 calcSquares 函数计算数的每个单独的数字的平方和,发送给 squareop 通道,类似地第 17 行 calcCubes 函数计算数的每个单的数字的立方和并发送给 cubeop 通道。

这两个函数在 31行和32行作为单独的 goroutine 被运行。每个被传递一个通道作为参数来写。main 协程在 33 行从这两个通道来等待数据。它们被存储在 squarescubes 变量,最后计算并打印。程序将打印

Final output 1536

死锁

使用通道必须要考虑的一个重要因素是死锁。如果一个通道在一个通道发送数据,然后它期待其他通道将接收数据,如果这些没有发生,程序将在运行时因 Deadlock 而 panic。

类似地如果一个协程等待从通道接收数据,则它期望在其他协程期望上写数据,否则程序将 panic

package main


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

上面程序,通道 ch 被创建,我们在 ch <-5 这一行向通道发送 5。在这个程序中没有其他协程从 ch 上读数据,因此程序将抛出如下运行时错误而 panic

fatal error: all goroutines are asleep - deadlock!

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

单向通道

目前我们所讨论的所有通道都是双向的。这样数据既可以在通道上发送,也可以被接收。也可以创建单向通道,这样通道仅可以发送或接收数据。

package main

import "fmt"

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

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

上面的程序我们在 10 行创建一个仅发送的通道 sendcnchan <- int 表示一个发送通道,箭头指向 chan。我们在 12 行试图从发送通道上面读数据,这是不允许的。当程序运行,编译器将抱怨

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

这很好,但是如果它不能被读取,向一个只写通道发送数据的意义是什么?

这就是通道转换要使用的地方。可以将双向通道转换为只写或只读通道,反之亦然

package main

import "fmt"

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

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

在上面程序的 10 行,双向通道 chn1 被创建。在 11 行,它作为参数传给 sendData 协程。sendData 函数在 5 行的参数 sendch chan <- int 将该通道转换为只写通道。所以现在通道在 sendData 协程内被转换为是只写通道,但它在 main 协程中是双向的。程序将打印 10

关闭通道和循环遍历通道

发送者有能力关闭通道来通知接收者通道上不再发送数据。

接收者在从通道上接收数据时,可以使用一个额外的变量来检查通道是否已被关闭。

v, ok := <- ch

在上面的语句中,如果从发送到通道成功接收到值,ok是 true。如果 ok 是 false,它意味着我们从一个关闭的通道上读取数据。从关闭的通道上接收到的值将会是通道类型的零值。例如,如果通道是 int 通道,则从关闭的通道上接收到的值是 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 协程将向通道 chn1 写入 0 到 9,然后关闭这个通道。主函数的 16 行有一个无限 for 循环,它在 18 行使用变量 ok 检查通道是否被关闭。如果 ok 是 false,它意味着通道被关闭,因此循环被中断。否则接收值和 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 结构的 for 循环可以被用来从一个通道上接收值,直到它被关闭。

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

在 16 行的 for range 循环从 ch 通道上接收数据,直到它被关闭。当 ch 被关闭,循环自动结束,程序打印

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

通道的另一个例子部分可以使用可重用的 for range 循环被重写

如果你仔细观察程序就可以注意到查找数的单个数字代码在 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 函数现在包含获得一个数的单个数字的逻辑,它被 calcSquarescalcCubes 函数并发地调用。如果数中没有其他数字,通道将在 13 行被关闭。calcSquarescalcCubes 协程使用 for range 循环监听它们各自的通道,直到它们被关闭。该程序的其他部分是相同的,程序将打印

Final output 1536

该教程也该结束了,通道还有其他一些概念例如有缓冲区的通道工作池select。我们将在下一部分讨论它们。感谢阅读

下一教程 - 有缓冲区的通道和工作池

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值