首页 > 编程笔记 > Go语言笔记 阅读:1

Go语言管道(schedule)的用法(附带实例)

管道是 Go 语言协程间通信的一种常用手段,可以分为无缓冲管道和有缓冲管道。

因为无缓冲管道本身没有容量,不能缓存数据,所以只有当有协程在等待读时,写操作才不会阻塞协程;或者当有协程在等待写时,读操作才不会阻塞协程。

因为有缓冲管道本身有一定容量,可以缓存一定数据,所以当协程执行写操作时,即使没有其他协程在等待读,只要管道还有剩余容量,写操作就不会阻塞协程;或者当协程执行读操作时,即使没有其他协程在等待写,只要管道还有剩余数据,读操作就不会阻塞协程。

下面写一个简单的 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

我们先向管道写入一个整型数据 100,再关闭管道,随后从管道读取两次数据。参考上面的输出结果,程序输出了两条语句,第一次正常读取到了数据 100,第二次读取到的是 0。

通过这个例子可以说明,即使管道关闭之后,也可以正常地从管道读取数据,没有数据时直接返回对应的空值(整型空值是 0,字符串空值是空字符串等)。

最后一个问题,如果关闭未初始化的管道,会怎么样呢?或者说再次关闭已关闭的管道,会怎么样呢?如果管道未初始化,关闭管道会导致程序抛 panic 异常(异常提示信息为 close of nil channel);如果管道已经被关闭,再次关闭管道也会导致程序抛 panic 异常(异常提示信息为 close of closed channel)。

相关文章