在上一篇教程中,我们讨论了如何使用 Goroutines 在 Go 中实现并发。在本教程中,我们将讨论 channel 以及 Goroutines 如何使用 channel 进行通信。
什么是 Channel
channel 可以看作是管道,Goroutines 使用管道进行通信。和水在管道中从一端流向另一端一样,数据可以通过管道从一端发送,从另一端接收。
声明 Channel
每个 channel 都有一个与之关联的类型。这种类型是 channel 允许传输的数据类型。不允许使用 channel 传输其他类型。
chan T 是 T
类型的 channel
channel 的零值为 nil
。nil
channel 没有任何作用,因此必须使用 make
来定义 channel ,跟定义 maps 和 slices 类似。
让我们编写声明 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 行中声明的 nil channel a
。因此,执行 if 条件中的语句并定义 channel 。上面程序中的 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 的方向指定数据是发送还是接收。
在第一行中,箭头从 a
指向内,因此我们从 channel a
读取数据并将值存储到变量 data。
在第二行,箭头指向 a
,因此我们向 channel a
写数据。
发送和接收在默认情况下是阻塞的
默认情况下,对 channel 的发送和接收是阻塞的。这是什么意思呢?当数据被发送到一个 channel 时,send 语句中被阻塞,直到其他 Goroutine 从该 channel 读取数据为止。类似地,当从 channel 读取数据时,读取将被阻塞,直到 Goroutine 将数据写入该 Channel 。
channel 的这一特性有助于 goroutine 在不使用显式锁或条件变量的情况下有效地通信,而显式锁或条件变量在其他编程语言中非常常见。
Channel 示例程序
前面讲了太多的理论:)。让我们编写一个程序来理解 Goroutine 如何使用 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")
}
这是上节课的程序。我们在这里使用 sleep 让主程序 Goroutine 等待 Goroutine hello 完成。如果你不理解这一点,我建议你阅读 Goroutines 的教程
我们将使用 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")
}
在上面的程序中,代码第 13 行我们创建了一个 done
bool channel 。 并将其作为参数传递给 hello
Goroutine。我们正在从 done
channel 接收数据。这行代码是阻塞的,这意味着在 Goroutine 将数据写入 done
channel 之前,程序不会执行下一行代码。因此,我们就不需要原始程序中的 time.Sleep 了。
代码行 <-done
从 done channel 接收数据,但不在任何变量中使用或存储该数据。这是完全合法的。
现在我们的 main
Goroutine 被阻塞等待 done channel 的数据。 hello
Goroutine接收此 channel 作为参数,输出 Hello world goroutine 然后写入 done
channel 。当这个写入完成时,main Goroutine 从 done
channel 接收数据,它被解锁,然后输出 main function。
程序输出
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")
}
在上面的程序第 10 行中,我们为第一行中的 hello
函数添加了 4 秒睡眠。
这个程序将首先输出 Main going to call hello go goroutine
。然后 hello Goroutine 将被启动,它将输出 hello go routine is going to sleep
。输出完后,hello
Goroutine 将休眠 4 秒,在此期间,main
Goroutine 将被阻塞,因为它在 <-done
中等待来自 done
channel 的数据。4 秒后,输出 hello go routine awake and going to write to done
,接着输出 Main received data
。
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
函数在第 7 行中计算组成一个数字的各个数的平方和,并将其发送到squareop
channel 。类似地,calcCubes
在第 17 中计算数字的各个数字的立方之和并将其发送到 cubeop
channel 。
这两个函数在第一行中作为单独的 Goroutines 运行。在第 31 和 32 行中,每个都输入一个 channel 作为参数。main Goroutine 等待来自这两个 channel 的数据。 第 33 行中一旦从两个 channel 接收到数据,它们就存储在 squares
和 cubes
变量中。最终输出。该程序将输出
Final output 1536
死锁
使用 channel 时要考虑的一个重要因素是死锁。如果 Goroutine 在 channel 上发送数据,那么其他 Goroutine 应该接收数据。如果没有发生这种情况,那么程序将在运行时陷入死锁。
类似地,如果一个 Goroutine 正在等待从一个 channel 接收数据,那么另一个 Goroutine 将在该 channel 上写入数据,否则程序将陷入异常。
package main
func main() {
ch := make(chan int)
ch <- 5
}
在上面的程序中,创建了一个 channel ch
,我们将 5
发送到 ch <- 5
。在这个程序中,没有其他 Goroutine 从 channel 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 时,chan<- int
表示只有发送数据的 channel 。我们试着从第 12 行只发送的 channel 中接收数据,这是不允许的,当程序运行时,编译器会报错
main.go:11: invalid operation: <-sendch (receive from send-only type chan<- int)
但是如果无法从发送 channel 读取数据,那么只向发送 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
函数将此 channel 转换为第 5 行中的仅发送数据 channel sendch chan<- int
。所以现在 channel 只在 sendData Goroutine中发送,但它在 main Goroutine 中是双向的。该程序将输出 10。
关闭 Channel 和 Channel 循环
发送方能够关闭 channel ,通知接收方 channel 上不再发送数据。
接收方可以在接收来自 channel 的数据时使用附加变量来检查 channel 是否已关闭。
v, ok := <- ch
在上面的语句中,如果通过成功的发送操作接收到某个 channel 的值,ok 为 true。如果 ok 为 false,则表示我们从一个已经关闭的 channel 读取值。从关闭的 channel 读取的值是 channel 类型的零值。例如,如果 channel 是 int
类型的 channel ,那么从关闭的 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 将 0 到 9 写入 chnl
channel ,然后关闭 Channel 。主函数在第 16 行中是一个无限 for
循环,它使用第 18 行中的变量 ok
检查 channel 是否关闭。如果 ok
为 false,则表示 channel 已关闭,因此循环中断。否则,将输出收到的值和 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 接收值,直到 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)
}
}
16 行的 for range
循环从 ch
channel 接收数据,直到它被关闭。一旦 ch
关闭,循环将自动退出。这个程序输出
Received 0
Received 1
Received 2
Received 3
Received 4
Received 5
Received 6
Received 7
Received 8
Received 9
可以使用 for range 循环重写另外一个channels 的程序,使其具有更多的代码重用性。
如果你仔细查看这个程序会注意到,在 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
函数同时调用。当数字中没有更多的数字时, channel 在第 13 行中关闭。calcSquares
和 calcCubes
Goroutines 使用 for range 循环监听它们各自的 channel ,直到它关闭为止。程序其余都是相同的。这个程序会输出
Final output 1536
这就是本教程的结尾。 channel 中还有其他概念,比如缓冲 channel 、工作池和 select。我们将在单独的教程中讨论它们。