Go教程-Channels第二部分|Go主题月

223 阅读5分钟

本文为翻译文章

原文地址:golangbot.com/channels/

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 中计算立方体,最后求和发生在主 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 函数会先判断 number 是否等于 0,如果等于 0 就发送 sum 的初始值到 squareop 通道里,如果不为 0 就计算 number 模 10 的 余数到平方和然后发送到 squareop 通道。calcCubes 函数也类似,只是它计算的事立方和。

这两个函数分别作为单独的 Goroutine 允许,并传入 channel 作为参数。然后主 Goroutine 从这两个通道中读取数据,分别存入 squares 和 cubes 变量。

程序输出:

Final output 1536  

死锁

使用通道时要考虑的一个重要因素是死锁。如果 Goroutine 正在通过通道发送数据,则预期其他 Goroutine 应该正在接收数据。如果预期的结果没有发生,则会发生死锁的 panic。

同样的,如果一个 Goroutine 向通道接收数据,则预期其他 Goroutine 应该正在向该通道发送数据,否则也会发送 panic 异常。

package main


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

在上面代码中,创建了一个 ch 的通道,然后用语句 ch <- 5 来向该通道写入数据 5 。在这个程序中,没有其他的 Goroutine 从 ch 通道中接收数据。因此这个程序将在运行时发生 panic 异常。

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:  
main.main()  
    /tmp/sandbox046150166/prog.go:6 +0x50

单向通道

到目前为止,我们讨论的所有通道都是双向通道,即可以在它们上发送和接收数据。也可以创建单向通道,即仅发送或接收数据的通道。

package main

import "fmt"

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

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

在上面代码中,我创建了一个只能发送数据的通道 sendch。语句 chan<- int 表示仅发送通道,因为箭头指向 chan。我尝试从该通道中读取数据,这是不允许的,并且在程序运行时,编译器会显示:

./prog.go:12:14: 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)
}

在上面代码中,创建了一个双向通道 chnl。它作为参数被传入到 sendData Goroutine 中。sendData 函数到参数 sendch chan<- int 会把该通道转换为只写入的单向通道。所以 sendch 通道在 Goroutine 中还是双向通道。

关闭通道和用于通道上的范围循环

发送者可以关闭该通道,以通知接收者该通道将不再发送任何数据。

接收器可以在从通道接收数据时使用附加变量,以检查通道是否已关闭。

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 Goroutine 向 chnl 写入 0-9 的数字,然后关闭该通道。主函数中定义了一个 for 循环,然后用 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 循环会一直从通道中接收数据直到该通道关闭。

让我们重写上面的的 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)
    }
}

for 循环从 ch 通道接收数据,直到关闭为止。 ch 关闭后,循环将自动退出。

该程序输出:

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

可以使用 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 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 Goroutines 使用 for 范围循环在其各自的通道上侦听,直到关闭为止。该程序的其余部分是相同的。

该程序将打印:

Final output 1536