channel
上一课,goroutine 算出的结果只能写进一个变量,main 靠 time.Sleep 猜它什么时候做完。channel 是更好的办法:它像一根管子,一个 goroutine 从一头放进一个值,另一个 goroutine 从另一头取出来。
1package main2 3import "fmt"4 5func main() {6 ch := make(chan string)7 go func() {8 ch <- "photo.jpg 下好了"9 }()10 msg := <-ch11 fmt.Println(msg)12}
photo.jpg 下好了
| 代码 | 意思 |
|---|---|
make(chan string) | 建一个能传 string 的 channel。 |
ch <- "…" | 发送:把值放进 channel。箭头指向 channel。 |
msg := <-ch | 接收:从 channel 取出一个值。箭头从 channel 指出来。 |
channel 的类型写成 chan 元素类型:chan int、chan string、chan bool。它和 map 一样要先 make 才能用。没 make 的 channel 是 nil,往它发送或从它接收都会永远等下去(不是 panic)。
接收会等
<-ch 的时候如果还没有值,接收的一方就停在那里等,直到有人发送。下面 total 在另一个 goroutine 里算订单的合计,main 等着收:
1package main2 3import "fmt"4 5func total(prices []int, out chan int) {6 sum := 07 for _, p := range prices {8 sum += p9 }10 out <- sum11}12 13func main() {14 out := make(chan int)15 go total([]int{12, 30, 8}, out)16 fmt.Println("等合计……")17 fmt.Println("合计", <-out)18}
等合计…… 合计 50
点单步:走到 <-out 时,main 那条泳道变成「卡在接收」,一根虚线箭头指着 out,说明它在等哪个 channel。这时 total 才轮到,算完合计发出去,main 收到值,接着往下走。不用猜时间:结果一到,main 马上就走,不早也不晚。
发送也会等
反过来也一样。make(chan T) 建的 channel 里面没有地方存值,所以发送的一方要等到有人来接收,两边碰上了,值才交过去。厨房做好一道菜,要等服务员来取:
1package main2 3import (4 "fmt"5 "time"6)7 8func main() {9 dishes := make(chan string)10 go func() {11 fmt.Println("厨房:番茄炒蛋做好了")12 dishes <- "番茄炒蛋"13 }()14 time.Sleep(time.Second)15 fmt.Println("服务员:来取菜")16 dish := <-dishes17 fmt.Println("服务员:端走了", dish)18}
厨房:番茄炒蛋做好了 服务员:来取菜 服务员:端走了 番茄炒蛋
点单步,看厨房那条泳道:它显示完第一行,走到 dishes <- "番茄炒蛋" 就变成「卡在发送」,在那里一直等到服务员(main)睡醒、走到 <-dishes。(网页上的 Sleep 不真的等。)
用 channel 等 goroutine 做完
很多时候不需要传什么结果,只想知道「做完了」。那就让 goroutine 做完以后往一个 channel 发一个值,main 收到就知道它做完了:
1package main2 3import "fmt"4 5func download(file string, done chan bool) {6 fmt.Println("下好了", file)7 done <- true8}9 10func main() {11 done := make(chan bool)12 go download("photo.jpg", done)13 <-done14 fmt.Println("main 结束")15}
下好了 photo.jpg main 结束
<-done 单独一行:收到的值不要了,只是等。和上一课的 time.Sleep 比一比:download 做多久,main 就正好等多久。而且 下好了 一定在 main 结束 前面:goroutine 发送之前做的事,main 收到以后一定都已经做完了。
收好几个结果
几个工人一起算平方,都往同一个 channel 里发。main 收几次,就加几次:
1package main2 3import "fmt"4 5func main() {6 nums := []int{3, 5, 8, 10}7 results := make(chan int)8 for _, n := range nums {9 go func() {10 results <- n * n11 }()12 }13 sum := 014 for range len(nums) {15 sum += <-results16 }17 fmt.Println("平方和", sum)18}
平方和 198
四个结果到达的先后在真的电脑上每次可能不一样,但加起来总是一样。猜一猜:
这段程序显示什么?
1package main2 3import "fmt"4 5func main() {6 ch := make(chan int)7 go func() {8 ch <- 109 }()10 go func() {11 ch <- 2012 }()13 fmt.Println(<-ch + <-ch)14}
两个 goroutine 各发一个值,main 收两次再相加。先收到哪个说不准,可 10 + 20 和 20 + 10 都是 30。
要是想按原来的顺序显示每个结果,可以给每个工人一个自己的 channel,再按顺序一个一个收:
1package main2 3import "fmt"4 5func main() {6 nums := []int{3, 5, 8}7 chans := make([]chan int, len(nums))8 for i, n := range nums {9 chans[i] = make(chan int)10 go func() {11 chans[i] <- n * n12 }()13 }14 for i, ch := range chans {15 fmt.Println(nums[i], "的平方是", <-ch)16 }17}
3 的平方是 9 5 的平方是 25 8 的平方是 64
点单步,看三个工人都卡在发送,每根箭头指着自己的 channel;main 先收 chans[0],再收 chans[1]……谁先算完都没关系,显示的顺序就是 nums 的顺序。
死锁
发送要等接收,接收要等发送。要是谁也等不到,会怎么样?
1package main2 3import "fmt"4 5func main() {6 ch := make(chan int)7 ch <- 428 fmt.Println(<-ch)9}
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main(...)
/tmp/sandbox/prog.go:7 +0x1d
exit status 2发生了什么: 死锁:所有 goroutine 都在等,谁也动不了(比如往没人收的 channel 发,或者 Wait 等不到 Done)。
main 停在第 7 行发送,等人来收。可是能收的只有它自己(下一行),别的 goroutine 一个都没有。Go 发现所有 goroutine 都在等、谁也不会再动,就报 fatal error: all goroutines are asleep - deadlock!(所有 goroutine 都睡着了:死锁)。下面的 goroutine 1 [chan send]: 说 main 卡在发送。点单步走到这一行,泳道变成红色的「全都卡住了」。
工人少了也一样:三个工人只发三个值,main 却要收四次,第四次就再也等不到了。死锁的更多样子见第 23 课。
单向 channel
把 channel 传给函数时,可以写明这个函数只发送还是只接收:chan<- int 只能发,<-chan int 只能收。写反了,编译器会拦住:
1package main2 3import "fmt"4 5func square(nums []int, out chan<- int) {6 for _, n := range nums {7 out <- n * n8 }9 fmt.Println(<-out)10}11 12func main() {13 out := make(chan int)14 go square([]int{2, 3}, out)15 fmt.Println(<-out, <-out)16}
# command-line-arguments
./prog.go:9:16: invalid operation: cannot receive from send-only channel out (variable of type chan<- int)发生了什么: out 是只能发的 channel(chan<-),不能从它收。
cannot receive from send-only channel out:out 在 square 里是只能发的。把第 9 行删掉就好了。普通的 chan int 传给 chan<- int 参数时自动变成单向的。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
两张订单一起算
✓total 已经写好,会把合计发进 out。在 main 里建两个 channel,开两个 goroutine 分别算两张订单,再收结果显示出来:订单 A 合计 70 和 订单 B 合计 155。
订单 A 合计 70 订单 B 合计 155
1package main2 3import "fmt"4 5func total(prices []int, out chan int) {6 sum := 07 for _, p := range prices {8 sum += p9 }10 out <- sum11}12 13func main() {14 orderA := []int{25, 40, 5}15 orderB := []int{120, 35}16 // 开两个 goroutine,再从 channel 收合计17 fmt.Println("订单 A 合计", 0)18 fmt.Println("订单 B 合计", 0)19}
修好它:多收了一次
✓sumOfSquares 给每个数开一个工人算平方,再把结果加起来。第一组数没问题,第二组却死锁了。改好它,让它显示 30 和 77。
30 77
1package main2 3import "fmt"4 5func sumOfSquares(nums []int) int {6 results := make(chan int)7 for _, n := range nums {8 go func() {9 results <- n * n10 }()11 }12 sum := 013 for range 4 {14 sum += <-results15 }16 return sum17}18 19func main() {20 fmt.Println(sumOfSquares([]int{1, 2, 3, 4}))21 fmt.Println(sumOfSquares([]int{4, 5, 6}))22}
按顺序报文件大小
✓给每个文件开一个 goroutine,把字节数换成 KB(除以 1024),发进它自己的 channel;main 按文件的顺序收,显示成 photo.jpg 2048 KB 这样,一个文件一行。
photo.jpg 2048 KB song.mp3 5120 KB notes.txt 10 KB
1package main2 3import "fmt"4 5func main() {6 files := []string{"photo.jpg", "song.mp3", "notes.txt"}7 sizes := []int{2097152, 5242880, 10240}8 chans := make([]chan int, len(files))9 // 给每个文件建 channel、开 goroutine10 for i := range chans {11 fmt.Println(files[i], sizes[i], "KB")12 }13}
这一课学会了
ch := make(chan int)建 channel;ch <- v发送,v := <-ch接收。- 没有缓冲的 channel:接收要等到有人发,发送要等到有人收,两边碰上才交值。
- 用 channel 等 goroutine 做完:
done <- true和<-done,不用猜时间。 - 几个工人往一个 channel 发:收几次就加几次;要按顺序,每个工人一个 channel,按顺序收。
- 谁也等不到谁:
fatal error: all goroutines are asleep - deadlock!。 - 单向 channel:
chan<- int只能发,<-chan int只能收。 - 单步时泳道写着「卡在发送」「卡在接收」,虚线箭头指着它等的 channel。