Golang教程:(二十二)信道
原文:https://golangbot.com/channels/ 欢迎来到Golang系列教程的第二十二篇。 在上一篇教程中,我们讨论了如何使用协程实现并发。在这篇教程中,我们将讨论信道以及如何使用信道实现协程间通信。 什么是信道信道(Channel)可以被认为是协程之间通信的管道。与水流从管道的一端流向另一端一样,数据可以从信道的一端发送并在另一端接收。 声明信道每个信道都有一个与之关联的类型。此类型是允许信道传输的数据类型,除此类型外不能通过信道传输其他类型。
信道的 0 值为 下面的代码声明了一个信道: 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)
}
}
在 Playground 中运行 因为信道的 0 值为 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
箭头的指向说明了数据是发送还是接收。 在第一行,箭头的方向是从 在第二行,箭头的方式是指向 发送和接收默认是阻塞的通过信道发送和接收数据默认是阻塞的。这是什么意思呢?当数据发送给信道后,程序流程在发送语句处阻塞,直到其他协程从该信道中读取数据。同样地,当从信道读取数据时,程序在读取语句处阻塞,直到其他协程发送数据给该信道。 信道的这种特性使得协程间通信变得高效,而不是向其他编程语言一样,显式的使用锁和条件变量来达到此目的。 信道的一个例子理论到此为止:) 让我们通过一个程序来理解协程之间如何使用信道进行通信。 我们将用信道来重写在上一篇教程协程中的一个例子。 如下是那篇教程中的一个例子: 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")
}
在 Playground 中运行 这是上一篇教程中的例子,我们通过使用 我们用信道重写上面的程序,如下: 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")
}
在 Playground 中运行 在上面的程序中,我们在第 12 行定义了一个 bool 类型的信道
现在我们的 程序的输出为: Hello world goroutine
main function
让我们修改上面程序,在 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")
}
在 Playground 中运行 在上面程序中的第 10 行,我们在 该程序首先打印 信道的另一个例子让我们再写一个例子来更好的理解信道。该程序打印一个数字的每一位的平方和与立方和,并将平方和与立方和相加得出最后的结果。 例如,输入123 ,程序将做如下计算以得出最后结果: squares = (1 * 1) + (2 * 2) + (3 * 3) 我们将平方和的计算与立方和的计算分别放在一个协程中执行,最后在主协程中将它们的计算结果求和。 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)
}
在 Playground 中运行 在第 7 行,函数 这两个函数接受不同的信道作为参数,并分别运行在各自的协程中(第31行和32行),最后将结果写入各自的信道。主协程在第 33 行同时等待这两个信道中的数据。一旦从这两个信道中接收到数据,它们分别被存放在变量 Final output 1536
死锁使用信道是要考虑的一个重要因素是死锁(Deadlock)。如果一个协程发送数据给一个信道,而没有其他的协程从该信道中接收数据,那么程序在运行时会遇到死锁,并触发 panic 。 同样地,如果一个协程从一个信道中接收数据,而没有其他的协程向这个信道发送数据,那么程序同样造成死锁,触发 panic 。 package main
func main() {
ch := make(chan int)
ch <- 5
}
在 Playground 中运行 上面的程序中,创建了一个信道 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)
}
在 Playground 中运行 在上面程序中的第 10 行,我们创建了一个只写(send only)信道 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)
}
在 Playground 中运行 在上面程序中的第 10 行,创建了一个双向信道 关闭信道以及使用 range for 遍历信道发送者可以关闭信道以通知接收者将不会再发送数据给信道。 在从信道接收数据时,接收者可以通过一个额外的变量来检测信道是否已经被关闭。 v,ok := <- ch 上面的语句中 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)
}
}
在 Playground 中运行 上面的程序中,协程 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
range 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)
}
}
在 Playground 中运行 在第6 行, Received 0 Received 1 Received 2 Received 3 Received 4 Received 5 Received 6 Received 7 Received 8 Received 9 在 信道的另一个例子 中的程序可以使用 range for 重写,以达到更多的代码可重用性。 如果仔细观察该程序,你可以注意到,在 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,squares+cubes)
}
在 Playground 中运行 在上面的程序中,函数 Final output 1536
这就来到了本教程的最后。信道中还有更多的概念,比如缓冲信道,工作池和 select 。我们将在单独的教程中讨论它们。感谢阅读,祝你有美好的一天。 目录 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |