在〈Goroutine〉中提到,想要通知主流程 Goroutine 已经结束,使用 Channel 是一种方式,实际上,Channel 是 Groutine 间的沟通管道。
使用 Channel
Channel 就像是个队列,可以对它发送值,也可以从它上头获取值,想要创建一个 Channel,要在类型之前加上个chan
,每个chan
都要定义可容纳的类型。
举例来说,使用 Channel 来修改之前的龟兔赛跑程序:
package main
import (
"fmt"
"math/rand"
"time"
)
func random(min, max int) int {
rand.Seed(time.Now().Unix())
return rand.Intn(max-min) + min
}
func tortoise(totalStep int, goal chan string) {
for step := 1; step <= totalStep; step++ {
fmt.Printf("乌龟跑了 %d 步...\n", step)
}
goal <- "乌龟"
}
func hare(totalStep int, goal chan string) {
flags := [...]bool{true, false}
step := 0
for step < totalStep {
isHareSleep := flags[random(1, 10)%2]
if isHareSleep {
fmt.Println("兔子睡着了zzzz")
} else {
step += 2
fmt.Printf("兔子跑了 %d 步...\n", step)
}
}
goal <- "兔子"
}
func main() {
goal := make(chan string)
totalStep := 10
go tortoise(totalStep, goal)
go hare(totalStep, goal)
fmt.Printf("%s 抵达终点\n", <-goal)
fmt.Printf("%s 抵达终点\n", <-goal)
}
在这个范例中,使用make
创建了一个 Channel,当乌龟或兔子抵达终点时,使用goal <-
发送一个字符串至 Channel 中,而在主流程中,使用<- goal
从 Channel 获取字符串,若 Channel 中无法获取数据,这时会发生阻断,直到可从 Channel 中获取字符串为止。实际上,使用goal <-
发送数据至 Channel 时,若 Channel 中已有数据,也会发生阻断,直到该数据被取走为止。
Buffered Channel
上头的范例创建 Channel 时并没有指定 Channel 中可以容纳多少数据,Channel 中默认只能容纳一个数据,你可以在创建 Channel 时指定当中可以容纳的数据数量。例如,创建一个生产者、消费者的程序:
package main
import "fmt"
func producer(clerk chan int) {
fmt.Println("生产者开始生产整数......")
for product := 1; product <= 10; product++ {
clerk <- product
fmt.Printf("生产了 (%d)\n", product)
}
}
func consumer(clerk chan int) {
fmt.Println("消费者开始消耗整数......")
for i := 1; i <= 10; i++ {
fmt.Printf("消费了 (%d)\n", <-clerk)
}
}
func main() {
clerk := make(chan int, 2)
go producer(clerk)
consumer(clerk)
}
在这个程序中,创建的 Channel 的容量为 2,因此在 Channel 的容量未满前,发送数据至 Channel 并不会发生阻断。
close 与 range
在这篇文件的第一个范例中,由于预期只会从 Channel 中收到两个字符串,因此主流程中使用了两次<- goal
,然而有时,我们无法事先知道,能从 Channel 得到几笔数据。
举例来说,你也许想写个猜数字游戏,在随机猜测数字的情况下,你无法事先知道要猜几次才会猜中,而你想将先前猜测的数字透过 Channel 传送:
package main
import (
"fmt"
"math/rand"
"time"
)
func random(min, max int) int {
rand.Seed(time.Now().Unix())
return rand.Intn(max-min) + min
}
func guess(n int, ch chan int) {
for {
number := random(1, 10)
ch <- number
if number == n {
close(ch)
}
time.Sleep(time.Second)
}
}
func main() {
ch := make(chan int)
go guess(5, ch)
for i := range ch {
fmt.Println(i)
}
fmt.Println("I hit 5....Orz")
}
在这个范例中,每次猜测的数字,都会使用ch <- number
传至 Channel 中,而最后猜中数字时,使用close()
关闭 Channel,Go 的range
可以搭配 Channel 使用,在 Channel 尚未关闭前,搭配for
就可以持续从 Channel 中取出数据。
select
如果有多个 Channel 需要协调,可以使用select
,直接来看个多个生产者与一个消费者的例子:
package main
import "fmt"
func producer(clerk chan int) {
fmt.Println("生产者开始生产整数......")
for product := 1; product <= 10; product++ {
clerk <- product
fmt.Printf("生产了 (%d)\n", product)
}
}
func consumer(clerk1 chan int, clerk2 chan int) {
fmt.Println("消费者开始消耗整数......")
for i := 1; i <= 20; i++ {
select {
case p1 := <-clerk1:
fmt.Printf("消费了生产者一的 (%d)\n", p1)
case p2 := <-clerk2:
fmt.Printf("消费了生产者二的 (%d)\n", p2)
}
}
}
func main() {
clerk1 := make(chan int)
clerk2 := make(chan int)
go producer(clerk1)
go producer(clerk2)
consumer(clerk1, clerk2)
}
在select
的case
中,会监看哪个 Channel 可以获取数据(或发送数据至 Channel),如果都有数据的话,就会随机选取,如果都无法获取数据(或发送数据至 Channel)就会发生 panic,这可以设置default
来解决,也就是监看的 Channel 中都没有数据的话就会执行,或者利用select
来做些超时设定。例如:
package main
import (
"fmt"
"math/rand"
"time"
)
func random(min, max int) int {
rand.Seed(time.Now().Unix())
return rand.Intn(max-min) + min
}
func producer(clerk chan int) {
fmt.Println("生产者开始生产整数......")
for product := 1; product <= 10; product++ {
time.After(time.Duration(random(1, 5)) * time.Second)
clerk <- product
fmt.Printf("生产了 (%d)\n", product)
}
}
func consumer(clerk1 chan int, clerk2 chan int) {
fmt.Println("消费者开始消耗整数......")
for i := 1; i <= 20; i++ {
select {
case p1 := <-clerk1:
fmt.Printf("消费了生产者一的 (%d)\n", p1)
case p2 := <-clerk2:
fmt.Printf("消费了生产者二的 (%d)\n", p2)
case <-time.After(3 * time.Second):
fmt.Printf("消费者抱怨中…XD")
}
}
}
func main() {
clerk1 := make(chan int)
clerk2 := make(chan int)
go producer(clerk1)
go producer(clerk2)
consumer(clerk1, clerk2)
}
如果过了 3 秒钟,另两个 Channel 都还是阻断,case <- time.After(3 * time.Second)
该行就会成立,因此就可以看到消费者的抱怨了…XD
在select
中若有相同的 Channel,会随机选取。例如底下会显示哪个结果是不一定的:
package main
import "fmt"
func main() {
ch := make(chan int, 1)
ch <- 1
select {
case <-ch:
fmt.Println("随机任务 1")
case <-ch:
fmt.Println("随机任务 2")
case <-ch:
fmt.Println("随机任务 3")
}
}
单向 Channel
可以将 Channel 转为只可发送或只可取值的 Channel,例如:
package main
import "fmt"
func producer(clerk chan<- int) {
fmt.Println("生产者开始生产整数......")
for product := 1; product <= 10; product++ {
clerk <- product
fmt.Printf("生产了 (%d)\n", product)
}
}
func consumer(clerk <-chan int) {
fmt.Println("消费者开始消耗整数......")
for i := 1; i <= 10; i++ {
fmt.Printf("消费了 (%d)\n", <-clerk)
}
}
func main() {
clerk := make(chan int, 2)
go producer(clerk)
consumer(clerk)
}
clerk chan<- int
是只能发送的 Channel,而clerk <-chan int
是只能接收的 Channel,从一个只能发送的 Channel 接收数据,或者是对一个只能接收的 Channel 发送数据,都会引发 invalid operation 的错误。
透过 Channel 来作为 Goroutine 间的沟通机制,是 Go 中比较建议的方式,如果你真的不想要透过 Channel,而想要直接共用某些数据结构,就必须注意有无 Race condition的问题,若必要,可透过锁定资源的方式来避免相关问题,有关锁定的方式,可以参考sync.Mutex的使用。