一个程序(函数)只干一件事,将不同的程序通过管道串联起来,完成一件复杂的事情。这是 pipeline 的核心思想。
1. 介绍
平时工作中,可能你会经常遇到这样的例子:
$ cat abc.log | grep "INFO" | awk '{print $5}'
abc.log 文件内容如下
// abc.log
[2018-06-05 21:30:22] [INFO] - id:4568 request:checkupdate response:version 2.2
[2018-06-06 11:30:52] [ERROR] - id:3322 request error
[2018-06-06 11:50:02] [ERROR] - id:9986 request error
[2018-06-07 01:40:13] [INFO] - id:1876 request:download response:version 2.2
[2018-06-07 02:30:44] [INFO] - id:5532 request:downlaod response:version 2.2
上面这一段连续的的命令,意思是将 abc.log 文件里带有 INFO 的行的第 5 列打印到屏幕。这样就把所有 INFO 行的 id 号打印出来了。
这种管线化思想在 golang 里也有体现。
2. golang 里的 pipeline
这里我们使用 Go 语言编程里的一个例子来说明。
Counter 是一个自然数生成器,从 0 开始,每次产生一个自然数。Squarer 对接收到的每个自然数做平方计算,然后将计算的结果丢给 Printer 打印到屏幕。
下面是实现代码:
package main
import "fmt"
func main() {
naturals := make(chan int)
squares := make(chan int)
// Counter
go func() {
for x := 0; ; x++ {
naturals <- x
}
}()
// Squarer
go func() {
for {
x := <-naturals
squares <- x * x
}
}()
for {
x := <-squares
fmt.Println(x)
}
}
不过我们的程序似乎不能结束!
3. 关闭 channel
3.1 close channel
假设我们只希望发送 0 到 99,然后就停止发送。你可能会说,这还不简单,把 Counter 改成下面这样:
go func() {
for x := 0; x < 100; x++ {
naturals <- x
}
}()
如果你真的这样写,你的程序在输出 100 个数的平方后,会报错:
图1 程序死锁
死锁的原因很简单,Counter 没有数据发送,导致 Squarer 卡在了从 naturals 接收数据上,间接导致 main 函数所在的主协程卡在从 squares 接收数据上。程序彻底死锁。
不过 Golang 比较厉害,能检测出这种情况。
解决死锁的办法是,关闭 channel. 你可以使用 close(natural)
来关闭. Counter 改为下面这样:
go func() {
for x := 0; x < 100; x++ {
naturals <- x
}
close(naturals)
}()
如果你运行这个程序,没有死锁,但是它也不会结束,而是永无何止的在屏幕上打印 0.
在 Golang 里,从一个已经关闭的 channel 读取数据,会读取到该 channel 对应数据类型的零值。如果向一个已经关闭的 channel 写数据,会引发 panic.
3.2 判断 channel 是否关闭
那如何判断 channel 是否关闭呢?难道检测是不是读取到零值?这显然不行,万一人家发的就是 0 呢?此时我们需要使用 channel 的第二种书写形式:
x, ok := <- naturals
如果 naturals 已经关闭,ok 就是 false. 我们再修改一版:
package main
import "fmt"
func main() {
naturals := make(chan int)
squares := make(chan int)
// Counter
go func() {
for x := 0; x < 100; x++ {
naturals <- x
}
close(naturals)
}()
// Squarer
go func() {
for {
x, ok := <-naturals
// 如果 ok 为 false,就关闭 squares 并退出循环
if !ok {
close(squares)
break
}
squares <- x * x
}
}()
for {
x, ok := <-squares
if !ok {
break
}
fmt.Println(x)
}
}
3.3 for range 迭代 channel
如果每次都使用 x, ok := <-naturals
这种语法来判断 channel 关闭,感觉有点麻烦,Golang 提供了更加简洁的语法,来简化这种操作:
// Counter
go func() {
for x := range naturals {
squares <- x * x
}
close(squares)
}
如果 naturals 被关闭,for 循环会自动退出。我们再修改一版:
package main
import "fmt"
func main() {
naturals := make(chan int)
squares := make(chan int)
// Counter
go func() {
for x := 0; x < 100; x++ {
naturals <- x
}
close(naturals)
}()
// Squarer
go func() {
for x := range naturals {
squares <- x * x
}
close(squares)
}()
for x := range squares {
fmt.Println(x)
}
}
4. 总结
- 掌握 pipeline 思想
- 掌握如何关闭 channel
- 掌握如何判断 channel 是否关闭
- 掌握 for range 迭代 channel 语法