本文为翻译文章
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