本文为翻译文章
在本教程中,我们将讨论通道以及Goroutines如何使用通道进行通信。
什么是 Channel
可以将通道视为使用 Goroutine 进行通信的管道。与水在管道中从一端流到另一端的方式类似,可以使用 Channel 从一端发送数据并从另一端接收数据。
声明 Channel
每个 Channel 都有其自己的类型,类型代表该 Channel 可以传输的数据类型。其他的数据类型不能使用与其 Channel 数据类型不同的 Channel 进行传输数据。
在 chan T 中,其中的 T 代表了这个 Channel 的类型。
Channel 的零值是 nil,值为 nil 的 Channel 不能被使用,所以 Channel 要像 Map 和 Slice 一样使用 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)
}
}
我们声明了一个 Channel a,其值为 nil。因此,将执行 if 条件中的语句并使用 make 定义 Channel。a 在上面代码中是一个 int 类型的 Channel。
该程序输出:
channel a is nil, going to define it
Type of a is chan int
通常,短的声明也是定义通道的有效且简洁的方法。
a := make(chan int)
上面代码也定义了一个Channel a。
Channel 发送数据和接收数据
下面给出了从通道发送和接收数据的语法:
data := <- a // read from channel a
a <- data // write to channel a
箭头相对于通道的方向指定是发送还是接收数据?
在第一行中,箭头从 a 指向外部,因此我们正在从通道 a 中读取并将值存储到变量数据中。
在第二行中,箭头指向 a,因此我们正在编写通道 a。
默认情况下,发送和接收到通道处于阻塞状态
默认情况下,发送和接收到通道处于阻塞状态。这是什么意思?当数据发送到通道时,控件将在 send 语句中被阻塞,直到其他 Goroutine 从该通道读取为止。同样,当从通道读取数据时,将阻止读取,直到某些 Goroutine 将数据写入该通道为止。
通道的此属性可帮助 Goroutine 在不使用其他编程语言中很常见的显式锁或条件变量的情况下有效地进行通信。
Channel 的示例代码
让我们编写一个程序来了解 Goroutines 如何使用通道进行通信。
实际上,我们将使用此处的通道来重写在学习 Goroutines 时编写的程序。
引用下上次的代码:
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 来让主协程进行睡眠,以至于让 go 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")
}
在上面代码中,我们创建了一个名为 done 的 bool 类型的通道,并将其作为参数传给了 go hello()。然后为们在下一行接收 done 通道的数据,程序会在其他 Goroutine 向该通道写入数据前一直被阻塞。因此,这消除了对 time.Sleep 的需求,这在原始程序中已经存在,可以防止主 Goroutine 退出。
代码 <-done 从完成的通道接收数据,但不使用该数据或将其存储在任何变量中。这是完全合法的。
现在,我们的主 Goroutine 已阻塞,以等待 done 通道上的数据。hello Goroutine 将此通道作为参数接收,打印 Hello world goroutine,然后写入 true 到 done 通道。写入完成后,主 Goroutine 从完成的通道接收数据,将其解除阻塞,然后打印 main function 文本。
程序输出:
Hello world goroutine
main function
让我们通过在 hello Goroutine 中引入睡眠来修改此程序,以更好地理解此阻塞概念。
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 协程中设置了睡眠 4 秒。
该程序首先会输出 Main going to call hello go goroutine。然后 hello 协程将会开始执行并输出 hello go routine is going to sleep。输出之后,hello 协程将会睡眠 4 秒种,在这期间 main 协程会一直被阻塞,直到可以从 done 中读取到数据。4 秒后,hello go routine awake and going to write to done 将会被输出,紧接着 Main received data 也会被输出。