在前面几篇通道教程中,我们陆续介绍了与通道相关的基本语法、单向通道以及 select 语句,有关通道的基本知识就介绍到这里,今天我们来看下通道使用过程中的错误和异常处理。
在并发编程的通信过程中,最需要处理的就是超时问题:比如向通道发送数据时发现通道已满,或者从通道接收数据时发现通道为空。如果不正确处理这些情况,很可能会导致整个协程阻塞并产生死锁。此外,如果我们试图向一个已经关闭的通道发送数据或关闭已经关闭的通道,也会引发 panic。以上都是我们在使用通道进行并发通信时需要尤其注意的。
接下来我们来看看如何解决上述问题。
超时处理机制实现
Go 语言没有提供直接的超时处理机制,但我们可以借助 select
语句来实现类似机制解决超时问题,因为 select
语句的特点是只要其中一个 case
对应的通道操作已经完成,程序就会继续往下执行,而不会考虑其他 case
的情况。基于此特性,我们来为通道操作实现超时处理机制,创建一个新的 Go 文件 channel5.go
,并编写代码如下:
package main
import (
"fmt"
"time"
)
func main() {
// 初始化 ch 通道
ch := make(chan int, 1)
// 初始化 timeout 通道
timeout := make(chan bool, 1)
// 实现一个匿名超时等待函数
go func() {
time.Sleep(1e9) // 睡眠1秒钟
timeout <- true
}()
// 借助 timeout 通道结合 select 语句实现 ch 通道读取超时效果
select {
case <- ch:
fmt.Println("接收到 ch 通道数据")
case <- timeout:
fmt.Println("超时1秒,程序退出")
}
}
使用 select
语句可以避免永久等待的问题,因为程序会在从 timeout
通道中接收到数据后继续执行,无论对 ch
的读取是否还处于等待状态,从而实现 1 秒超时的效果。这种写法看起来是一个编程小技巧,但却是在 Go 语言并发编程中避免通道通信超时的最有效方法。
执行上述代码,打印结果如下:
超时1秒,程序退出
而如果没有 timeout
通道和上述 select
机制,从 ch
通道接收数据会得到如下 panic(死锁):
fatal error: all goroutines are asleep - deadlock!
避免对已关闭通道进行操作
为了避免对已关闭通道再度执行关闭操作引发 panic,一般我们约定只能在发送方关闭通道,而在接收方,我们则通过通道接收操作返回的第二个参数是否为 false
判定通道是否已经关闭,如果已经关闭,则不再执行发送操作,示例代码 channel6.go
如下:
package main
import "fmt"
func main() {
ch := make(chan int, 2)
// 发送方
go func() {
for i := 0; i < 5; i++ {
fmt.Printf("发送方: 发送数据 %v...\n", i)
ch <- i
}
fmt.Println("发送方: 关闭通道...")
close(ch)
}()
// 接收方
for {
num, ok := <-ch
if !ok {
fmt.Println("接收方: 通道已关闭")
break
}
fmt.Printf("接收方: 接收数据: %v\n", num)
}
fmt.Println("程序退出")
}
上述代码执行结果如下:
如果我们试图在通道 ch
关闭后发送数据到该通道,则会得到如下 panic:
panic: send on closed channel
而如果我们试图在通道 ch
关闭后再次关闭它,则会得到如下 panic:
panic: close of closed channel
推荐阅读
喜欢本文的朋友,欢迎关注“Go语言中文网”:
Go语言中文网启用微信学习交流群,欢迎加微信:274768166
文章评论