Go教程第十九篇: Channels
本文是《Go系列教程》的第十九篇文章。
在上一篇文章中,我们讨论了并发是如何使用Goroutine实现的。在本文中,我们将详细讨论channel的有关内容,以及Goroutine是如何利用channel完成通信的。
什么是Channel ?
你可以把channel看成是管道,利用channel,goroutine完成彼此通信。和管道中水流从一端留到另一端类似,使用channel,你可以从一端发出数据,而在另一端完成数据的接收。
声明Channel
每一个channel都有一个相关的类型。此类型即是channel所允许传输的数据类型,除此之外的其他数据类型不允许使用此channel传输。
chan T 即: 数据类型为T的channel。
channel的零值是nil。nil值的channel毫无用处,因此,类似于map和slice,channel必须使用make进行定义。
我们来写段代码声明一个channel。
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)
}
}
var a chan int我们在这行代码中声明的channel a是一个零值的channel,它的值为nil。因此,if条件中的语句会执行,channel a会被定义出来。程序中的a是一个int 类型的channel。程序的输出如下:
channel a is nil, going to define it
Type of a is chan int
和往常一样,我们也可以使用快捷的语法来声明,声明channel的简洁方式是:
a := make(chan int)
上面的这行代码,也是定义了一个int类型的channel a。
从channel中发送和接收数据
从channel中发送和接收数据的语法如下:
data := <- a // read from channel a
a <- data // write to channel a
上面的代码中,和channel相关的箭头的方向,就规定了数据是发送给channel还是从channel进行数据接收。
第一行代码中,箭头是由a发出的,因此,我们是从channel a中读取数据,并把读取的数据存储到变量data中。在第二行代码中,箭头指向了a,因此,我们是在把数据写入到channel a中。
默认情况下,发送和接收都是阻塞的
channel的收发操作默认都是阻塞式的,什么意思呢? 当把数据发送给channel的时候,程序会一直阻塞在发送语句那儿,直到有其他Goroutine从该channel中执行读取操作。同样类似地,当从channel中读取数据的时候也是一样,读取操作会被一直阻塞住,直到有其他Goroutine向该channel中写入数据。
channel的这个特性可以在不使用显示的锁或条件变量的情况下,有效地解决Goroutine的通信问题。
Channel使用案例
好了,到现在为止,我们已经将了足够的理论,现在让我们写段程序理解下,Goroutine是如何使用channel完成通信的。
我们把上一节的程序拿过来,改写一下。
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")
}
现在,我们使用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")
}
在上面的程序中,我们创建了一个bool类型的channel done,并把它作为参数传递给hello Goroutine。在第14行,我们从done channel中接收数据,这行代码会被阻塞住,即:直到有其他Goroutine向该channel中写入数据。其他Goroutine向done channel中写入数据时,程序才会往下执行。因此,这样就消除了time.sleep的使用。
<-done在这行代码中,我们从done channel中读取数据,但是并不把读取的数据存储到任何变量中,这样也是完全合法的。
现在,我们的main Goroutine会在done channel上阻塞住,一直等待来自done的数据。hello Goroutine把此channel作为参数接收,打印了“Hello world goroutine”。之后,向done channel中写入数据,当写入完成之后,main Goroutine就接收到了来自done channel的数据,它就会解除阻塞,继续往下执行。
程序的输出如下:
Hello world goroutine
main function
为了更好地理解阻塞概念,我们修改下这个程序,我们在hello Goroutine中引入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函数中调用了sleep方法,睡眠了4秒。
程序会先输出Main going to call hello go goroutine,之后,hello Goroutine被启动,输出:hello go routine is going to sleep。在这完成之后,hello Goroutine会在done睡眠4秒,在此期间,main Goroutine将被阻塞住,因为它在调用<-done之后,就会一直等待来自done channel的数据。4秒之后,紧接着:Main received data 之后,将会输出:hello go routine awake and going to write to done。
另一个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中,和的计算放在main 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函数会计算给定数字上的每一个数位的平方和,并把计算结果发送给channel squareop。类似地, calcCubes 函数会计算每一个数位上数字的立方和,并把计算结果发送给cubeop通道。
这俩个函数都会运行在单独的Goroutine里面,这俩个函数都接收了一个channel作为参数。main函数的Goroutine会等待从这俩个channel中获取数据,一旦从这俩个channel中收到数据之后,它们就会被存储在squares和cubes变量中,程序的最终输出如下:
Final output 1536
死锁
我们在使用channel时,需要考虑到的一个重要情况就是死锁问题。如果一个Goroutine正在向通道中发送数据,之后,它就会等待着其他Goroutine接收此数据的。如果没有其他Goroutine从channel中获取数据,程序就会在运行时发生死锁问题。
类似地,如果一个Goroutine等待着从channel中接收数据的话,而此时就必须得有其他Goroutine向此channel中写入数据。否则的话,程序就会出错。
package main
func main() {
ch := make(chan int)
ch <- 5
}
在上面的程序中,会创建一个名为ch的channel。之后,我们把5发送给此通道。但是在此程序中,没有其他Goroutine从名为ch的channel中获取数据。因此程序就会在运行时发生错误。
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/tmp/sandbox249677995/main.go:6 +0x80
单向通道
我们上面讨论的channel都是双向的channel。双向channel我们不仅可以向channel中发送数据,同时还可以从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,名为sendch。chan<- int表明了这是一个单向发送channel,箭头指向chan。在紧接着的一行,我们试图从一个单向发送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)
}
在程序的第10行,我们创建了一个双向的channel。然后我们把它作为参数传递给sendData的Goroutine。此时sendData函数会把此channel转换成一个单向只读channel。
因此,在sendData的Goroutine内部,此channel只是一个单向只读channel。但是在main函数的Goroutine里面,它确是一个双向channel。程序会打印出10作为输出。
关闭通道和for循环遍历通道
发送方可以主动地关闭通道去通知接收者此channel中不再发送数据。在从channel中获取数据的时候,接收者可以使用额外的变量来判断此channel是否已经关闭。
v, ok := <- ch
在上面的程序中,如果我们接收的是成功发送到channel中的数据的话,OK的值为true。如果OK的值是false,那也就意味着我们正在从一个关闭的通道中读取数据。从一个已关闭的channel中读取到的值为此channel类型的零值。例如,如果channel是int类型的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)
}
}
在上面的程序中,producer的Goroutine会向chnl通道中写入0-9,之后关闭此通道。main函数里面的for循环会检查通道是否已经关闭,如果已经关闭的话,就跳出循环,否则的话,就把
接收到的值打印出来。
程序的输出如下:
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循环重写,以提高代码复用性。
如果你仔细观察程序的话,你会看到查找一个数的每一个数位上的代码的函数在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函数封装了从一个数字中提取每个数位上数字的代码。它被calcSquares 和 calcCubes函数并发地调用。一旦此数字上没有更多的数位时,通道就会关闭掉。
calcSquares 和 calcCubes的Goroutine会使用for range 循环监听他们各自的channel,直到通道关闭。程序的剩下部分基本上都一样。程序将输出如下:
Final output 1536
感谢您的阅读,请留下您珍贵的反馈和评论。Have a good Day!
备注
本文系翻译之作原文博客地址
网友评论