Go 语言并发编程系列(八)—— 通道类型篇:错误和异常处理

在前面几篇通道教程中,我们陆续介绍了与通道相关的基本语法、单向通道以及 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

本文分享自微信公众号 - 学院君的后花园(geekacademy)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-09-02

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏poslua

Golang Dep 依赖冲突处理

对于 Golang 应用内存堆栈的监控,基本都是读取 runtime.MemStats,然后发往一些 TSDB 进行可视化展示。代码一般都是这样的:

11720
来自专栏poslua

Golang -ldflags 的一个技巧

我在开发 go 的项目时,习惯上会在编译后的二进制文件中注入 git 等版本信息后再发布。一直以来都是这么做的:

11020
来自专栏有困难要上,没有困难创造困难也要上!

Singularity升级

以前装的Singularity版本比较低,最近要用新功能,只能升级了。因为以前Singularity安装的时候是使用自己编译的rpm安装的,所以要安装新版本,需...

7610
来自专栏poslua

IPv4 也是可以访问 IPv6 服务的

对于 Golang 的 net.Listen() 函数,如果你不强行指定 IPv4 或 IPv6 的话,在双栈系统上默认只会监听 IPv6 地址。比如,用 Go...

96630
来自专栏区块链入门

【实践】使用Go pprof做内存性能分析

阿里云Redis线上在某些任务流中使用redis-port来进行实例之间的数据同步。redis-port是一个MIT协议的开源软件,主要原理是从源实例读取RDB...

18610
来自专栏区块链入门

【实践】Golang的单元测试入门go test

go test命令,相信大家都不陌生,常见的情况会使用这个命令做单测试、基准测试和http测试。go test还是有很多flag 可以帮助我们做更多的分析,比如...

21210
来自专栏poslua

谈谈 Golang 中的 Data Race

我在接手其他同事的 golang 项目时,一般都会习惯性的做一个竞态检测。有时总会得到一些“惊喜”,比如像下面这段代码:

17630
来自专栏golang工程实践

golang工程通用构建方式

要解决上述的问题,我们需要一个构建脚本/工具来自动化的在开发、持续集成、预发布阶段提供下列功能:

22560
来自专栏poslua

谈谈 Golang 中的 Data Race(续)

我在上一篇文章中曾指出:在 Go 的内存模型中,有 race 的 Go 程序的行为是未定义行为,理论上出现什么情况都是正常的。并尝试通过一段有 data rac...

9440
来自专栏区块链入门

【实践】Golang的goroutine和通道的8种姿势

如果说php是最好的语言,那么golang就是最并发的语言。 支持golang的并发很重要的一个是goroutine的实现,那么本文将重点围绕goroutin...

12210

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励