- 原文地址:Part 22: Channels
- 原文作者:Naveen R
- 译者:咔叽咔叽 转载请注明出处。
在上一个教程中,我们讨论了 Go 中如何使用Goroutines实现并发。在本教程中,我们将讨论有关channel以及Goroutines如何使用channel进行通信。
什么是channel
channel可以被认为是Goroutines通信的管道。类似于水在管道中从一端流到另一端的方式,数据可以从一端发送,可以从另一端接收。
channel的声明
每个channel都有一个与之关联的类型。此类型是允许channel传输的数据类型。不允许使用该channel传输其他类型。
chan T 代表类型为T的channel
channel的零值为nil。nil channel没有任何用处,因此得使用类似于make map和 make slice来定义它。
让我们写一些声明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)
}
}
在第 6 行声明了var a chan int,可以看到channel的零值为nil。因此,执行if条件内的语句并定义channel。上面的程序中的a是一个int channel。该程序将输出,
channel a is nil, going to define it
Type of a is chan int
使用make声明也是定义channel的有效而简洁的方法。
a := make(chan int)
上面的代码行定义了一个int型的channel a
channel的发送和接收
下面给出了从channel发送和接收数据的语法,
data := <- a // read from channel a
a <- data // write to channel a
箭头相对于通道的方向指定了是发送还是接收数据。
在第 1 行中,箭头从a向指向data,因此我们从通道a读取并将值存储到变量data中。
在第 2 行中,箭头指向a,因此我们把data写入通道a。
发送和接收默认是阻塞的
默认情况下,发送和接收是阻塞的。这是什么意思?当数据发送到channel时,发送方被阻塞直到其他Goroutine从该channel读取出数据。类似地,当从channel读取数据时,读取方被阻塞,直到其他Goroutine将数据写入该channel。
channel的这种属性有助于Goroutines有效地进行通信,而无需使用在其他编程语言中常见的显式锁或条件变量。
channel示例代码
让我们编写一个程序来了解Goroutines如何使用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")
}
Run in playgroud
这是上一个教程的代码,这里我们将使用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型的done channel。 并将其作为参数传递给hello。第 14 行我们正在从done channel接收数据。这行代码是阻塞的,这意味着在Goroutine将数据写入done channel之前将会一直阻塞。因此,上一个程序中的time.Sleep的就没有必要了,用sleep对程序而言是相当不友好。
代码行<-done表示从done channel接收数据,如果没有任何变量使用或存储该数据,这是完全合法的。
现在我们的main Goroutine被阻塞直到done channel有数据写入。 hello Goroutine接收done channel作为参数,打印Hello world goroutine然后把true写入done channel。当这个写入完成时,main Goroutine从该done channel接收数据,然后结束阻塞打印了main函数的文本。
输出,
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")
}
这个程序将首先打印Main going to call hello go goroutine。然后hello Goroutine启动,打印hello go routine is going to sleep。打印完成后,hello Goroutine将休眠 4 秒钟,在此期间main Goroutine将被阻塞,因为它正在等待来自<-done的通道的数据。 4 秒后hello Goroutine苏醒,然后打印hello go routine awake and going to write to done并写入数据到channel,接着main Goroutine接收数据并打印Main received data。
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函数计算各个数字的平方的和,并将其发送到squares channel。类似地,calcCubes计算各个数字的立方的和并将其发送到cubes channel。
这两个函数都作为单独的Goroutines运行。每个函数都通过一个channel作为入参。main Goroutine等待来自这两个channel的数据。一旦从两个channel接收到数据,它们就存储在squares和cubes中求和,然后打印最终输出。该程序将打印,
Final output 1536
死锁
使用channel时要考虑的一个重要因素是死锁。如果Goroutine正在channel上发送数据,那么期待其他一些Goroutine接收数据。如果发送的数据没有被消费,程序将在运行时产生一个panic。
同样,如果Goroutine正在等待从一个channel接收数据,那么其他Goroutine应该在该channel上写入数据,否则程序也会出现panic。
package main
func main() {
ch := make(chan int)
ch <- 5
}
在上面的程序中,创建了一个channel ch,我们用ch <-5向channel发送 5。在该程序中,没有其他Goroutine从ch接收数据。因此,此程序将出现以下运行时错误。
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/tmp/sandbox249677995/main.go:6 +0x80
单向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)
}
在上面的程序中,我们在第 10 行中创建了仅发送channel sendch。chan < - int表示当箭头指向chan时仅为发送channel。我们在第 12 行中尝试从该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 chnl。在第 11 行,它作为参数传递给sendData Goroutine,而sendData函数在第 5 行用sendch chan < - int将此chnl转换为仅发送的channel类型。所以现在通道只在sendData Goroutine中是单向的,但它在main Goroutine中是双向的。该程序将打印 10 作为输出。(译者注:这就是单向channel的用途,定义函数或者方法的时候,使用只读或只写会让代码更健壮。)
关闭channel和循环channel
发送者能够关闭channel以通知接收者不再在该channel上发送数据。
接收者可以在从channel接收数据时使用额外的变量来检查channel是否已关闭。
v, ok := <- ch
在上面的语句中,如果成功地从该操作中接收到该值,则ok为true。如果ok为false,则表示我们正在从一个关闭的channel中读取。从关闭的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)
}
}
在上面的程序中,生产者Goroutine将 0 到 9 写入channel chnl,然后关闭它。在第 16 行main函数有一个无限for循环,它使变量ok检查channel是否被关闭。如果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 range形式可用于从channel接收值,直到它被关闭。
让我们使用for range循环重写上面的程序。
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 range循环在第 16 行接收来自channel ch的数据直到它被关闭。 ch关闭后,循环自动退出。该程序输出,
Received 0
Received 1
Received 2
Received 3
Received 4
Received 5
Received 6
Received 7
Received 8
Received 9
我们来重写一下上面那个求平方立方和的程序,
如果仔细查看程序,可以注意到在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函数现在包含从number中获取各位的逻辑,并且它同时由calcSquares和calcCubes函数调用。一旦number中没有更多的位,channel就会在第 13 行被关闭。 calcSquares和calcCubes Goroutines使用for range循环监听各自的channel,直到它关闭。该程序的其余部分和之前的例子是相同的。该程序也会打印
Final output 1536
该节教程就结束了,channel中还有更多的概念,例如缓冲channel,worker pool和select。我们将在下一个教程中讨论它们。