Go语言管道(schedule)的用法(附带实例)
管道是 Go 语言协程间通信的一种常用手段,可以分为无缓冲管道和有缓冲管道。
因为无缓冲管道本身没有容量,不能缓存数据,所以只有当有协程在等待读时,写操作才不会阻塞协程;或者当有协程在等待写时,读操作才不会阻塞协程。
因为有缓冲管道本身有一定容量,可以缓存一定数据,所以当协程执行写操作时,即使没有其他协程在等待读,只要管道还有剩余容量,写操作就不会阻塞协程;或者当协程执行读操作时,即使没有其他协程在等待写,只要管道还有剩余数据,读操作就不会阻塞协程。
下面写一个简单的 Go 程序,学习管道的基本用法,代码如下所示:
主协程休眠 1s 是为了防止主协程结束,整个 Go 程序退出,导致子协程也提前结束。函数 make 用于初始化 Go 语言的一些内置类型,如切片 slice、散列表 map 以及管道 chan。
注意,用函数 make 初始化时,第一个参数 chan int 表示管道只能用来传递整型数据,第二个参数表示管道的容量是 1,即最多只能缓存一个整型数据。
管道的操作还是比较简单的,无非就是读、写以及关闭操作。这里提出一个问题,如果程序没有初始化管道,却执行读或者写操作会发生什么呢?或者说,如果一个管道已经被关闭了,这时候执行读或者写操作会发生什么呢?
我们写一些简单的 Go 程序测试一下:
1) 不初始化管道,直接执行写操作,代码与运行结果如下所示:
为什么所有的协程都在休眠呢?其实是由主协程向未初始化的管道写数据导致的,也就是说,向未初始化的管道写数据会导致协程永久性阻塞。
2) 不初始化管道,直接执行读操作,代码与运行结果如下所示:
3) 关闭管道之后,再执行写操作,代码与运行结果如下所示:
4) 关闭管道之后,再执行读操作,代码与运行结果如下所示:
通过这个例子可以说明,即使管道关闭之后,也可以正常地从管道读取数据,没有数据时直接返回对应的空值(整型空值是 0,字符串空值是空字符串等)。
最后一个问题,如果关闭未初始化的管道,会怎么样呢?或者说再次关闭已关闭的管道,会怎么样呢?如果管道未初始化,关闭管道会导致程序抛 panic 异常(异常提示信息为 close of nil channel);如果管道已经被关闭,再次关闭管道也会导致程序抛 panic 异常(异常提示信息为 close of closed channel)。
因为无缓冲管道本身没有容量,不能缓存数据,所以只有当有协程在等待读时,写操作才不会阻塞协程;或者当有协程在等待写时,读操作才不会阻塞协程。
因为有缓冲管道本身有一定容量,可以缓存一定数据,所以当协程执行写操作时,即使没有其他协程在等待读,只要管道还有剩余容量,写操作就不会阻塞协程;或者当协程执行读操作时,即使没有其他协程在等待写,只要管道还有剩余数据,读操作就不会阻塞协程。
下面写一个简单的 Go 程序,学习管道的基本用法,代码如下所示:
package main import ( "fmt" "time" ) func main() { queue := make(chan int, 1) go func() { for { data := <-queue // 读取 fmt.Print(data, " ") // 0 1 2 3 4 5 6 7 8 9 } }() for i := 0; i < 10; i++ { queue <- i // 写入 } time.Sleep(time.Second) }参考上面的代码,主协程循环向管道写入整数,子协程循环从管道读取数据。
主协程休眠 1s 是为了防止主协程结束,整个 Go 程序退出,导致子协程也提前结束。函数 make 用于初始化 Go 语言的一些内置类型,如切片 slice、散列表 map 以及管道 chan。
注意,用函数 make 初始化时,第一个参数 chan int 表示管道只能用来传递整型数据,第二个参数表示管道的容量是 1,即最多只能缓存一个整型数据。
管道的操作还是比较简单的,无非就是读、写以及关闭操作。这里提出一个问题,如果程序没有初始化管道,却执行读或者写操作会发生什么呢?或者说,如果一个管道已经被关闭了,这时候执行读或者写操作会发生什么呢?
我们写一些简单的 Go 程序测试一下:
1) 不初始化管道,直接执行写操作,代码与运行结果如下所示:
package main import "fmt" func main() { var queue chan int queue <- 100 fmt.Println("main end") }运行结果:
fatal error: all goroutines are asleep - deadlock!
运行上面的程序,竟然报错了,提示 all goroutines are asleep,意思是所有的协程都在休眠,程序死锁了。为什么所有的协程都在休眠呢?其实是由主协程向未初始化的管道写数据导致的,也就是说,向未初始化的管道写数据会导致协程永久性阻塞。
2) 不初始化管道,直接执行读操作,代码与运行结果如下所示:
package main import "fmt" func main() { var queue chan int data := <-queue fmt.Println("main end", data) }运行结果:
fatal error: all goroutines are asleep - deadlock!
可以看到,第 2 个程序的运行结果与第 1 个程序一致,主协程同样被阻塞了,即从未初始化的管道读数据也会导致协程的永久性阻塞。3) 关闭管道之后,再执行写操作,代码与运行结果如下所示:
package main import "fmt" func main() { queue := make(chan int, 1) close(queue) queue <- 100 fmt.Println("main end") }运行结果:
panic: send on closed channel
运行上面的程序,你会发现程序抛出 panic 异常并退出了,异常提示信息为 send on closed channel,意思是向已关闭管道写数据。也就是说,向已关闭的管道写数据会导致程序抛 panic 异常。4) 关闭管道之后,再执行读操作,代码与运行结果如下所示:
package main import "fmt" func main() { queue := make(chan int, 1) queue <- 100 close(queue) data1 := <-queue fmt.Println("main end1", data1) data2 := <-queue fmt.Println("main end2", data2) }运行结果:
main end1 100
main end2 0
通过这个例子可以说明,即使管道关闭之后,也可以正常地从管道读取数据,没有数据时直接返回对应的空值(整型空值是 0,字符串空值是空字符串等)。
最后一个问题,如果关闭未初始化的管道,会怎么样呢?或者说再次关闭已关闭的管道,会怎么样呢?如果管道未初始化,关闭管道会导致程序抛 panic 异常(异常提示信息为 close of nil channel);如果管道已经被关闭,再次关闭管道也会导致程序抛 panic 异常(异常提示信息为 close of closed channel)。