golang使用通道时需要注意的一些问题

 更新时间:2023年07月04日 10:42:01   作者:自由de单车  
本文主要介绍了golang使用通道时需要注意的一些问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

环境

  • Go 1.20
  • Windows 11

常识

1.定义通道变量:

ch := make(chan int) // 可存放int类型数据,缓冲为0
ch := make(chan any) // 可存放任意类型数据,缓冲为0
ch := make(chan int, 5) // 存放int类型数据,缓冲为5
// 默认的通道是既可以写入又可以读取的,但我们也可以限制通道的方向
ch := make(<-chan int) // 只能从此通道读取数据,且不能关闭此通道
ch := make(chan<- int) // 只能写入数据到此通道
length := len(ch) // 通道里有多少个数据
capacity := cap(ch) // 通道的缓冲区大小

2.通道遵循FIFO先入先出规则,可以保证元素的顺序

3.通道是并发安全的,不会因多个协程的同时写入而发生数据错乱

注意点

下面的代码例子会经常出现调用display函数,这是我自己定义的一个函数,主要用于打印信息,代码如下:

func display(msg ...any) {
    fmt.Print(time.Now().Format(time.DateTime), " ")
    fmt.Println(msg...)
}

为了减少代码冗余,下面的代码例子就不再贴出此函数的代码了。

1、对一个没有关闭的通道进行读写时,如果遇上了阻塞,并且此时已经没有其它活跃(非阻塞)的协程在运行了,会报deadlock错误!

怎么理解这句话呢,首先要了解读写通道时什么情况下会阻塞:

  • 往缓冲已满的通道写入数据时会阻塞
  • 读取空的通道会阻塞
  • 通道未初始化,例如var ch chan int就是未初始化的

针对第1点,假设通道缓冲是N,那么在第 N + 1 次写入时会阻塞(定义通道变量时如果不指定N的大小,则N默认等于0)

针对第2点,如果这个空的通道是已关闭的,则不会阻塞,读取到的是这个通道数据类型的零值

例子1:

func main() {
    ch := make(chan int)
    // 协程1
    go func() {
        for i := 0; i < 3; i++ {
            display("准备发送:", i)
            ch <- i
            display("已发送完毕:", i)
        }
    }()
    for data := range ch {
        display("获得数据:", data)
    }
}

上面代码运行后会报错:fatal error: all goroutines are asleep - deadlock!

原因是,当【协程1】往通道写入3个数据后,【协程1】就结束运行了,这时【main协程】(是的,main函数也是运行在协程里的)读取出这3个数据后,并没有退出for-range循环,而是继续读取已空的ch通道,发生了阻塞,但这时只有【main协程】在运行了,只剩下一个协程,所以报错。

例子1修改一下:

func main() {
    ch := make(chan int)
    // 协程1
    go func() {
        for i := 0; i < 3; i++ {
            display("准备发送:", i)
            ch <- i
            display("已发送完毕:", i)
        }
    }()
    // 协程2
    go func() {
        for data := range ch {
            display("获得数据:", data)
        }
    }()
    // 死循环
    for {
    }
}

经修改后代码不会再报错了,原因是,【协程1】退出后,虽然【协程2】还在阻塞式地读取空通道,但这时除了【协程2】以外,还有一个活跃的【main协程】在运行,所以不会报错。

例子1再修改下:

func main() {
    ch := make(chan int)
    // 协程1
    go func() {
        for i := 0; i < 3; i++ {
            display("准备发送:", i)
            ch <- i
            display("已发送完毕:", i)
        }
        close(ch) // 新添加代码
    }()
    for data := range ch {
        display("获得数据:", data)
    }
}

协程1在写入完所有数据后,使用close(ch)关闭了通道,这时也不会再报错了。原因是,对于已关闭的通道,for-range循环读取完通道的数据后,会自动结束循环,不会阻塞在读取通道处,所以不会报错。

2、给一个已关闭的通道发送数据,或者再次关闭一个已关闭的通道,会导致panic

这句话告诉我们,当发送方不再需要发送数据时,可以关闭通道,但不能让接收方去关闭。
因为接收方并不知道发送方是否还需要发送数据,如果胡乱关闭了通道,会导致发送方触发panic

3、已关闭的通道是可以继续读取里面的数据的

func main() {
    ch := make(chan int, 2)
    ch <- 123
    ch <- 456
    close(ch)
    // 使用for-range读取已关闭通道,通道空了之后会自动跳出循环
    for data := range ch {
        display(data)
    }
    // 方式2:使用ok变量判断通道是否已空
    /*for {
        data, ok := <-ch
        if !ok {
            break
        }
        display(data)
    }*/
    // 方式3:通过通道长度来判断通道是否已空
    /*num := len(ch)
    for i := 0; i < num; i++ {
        data := <-ch
        display(data)
    }*/
}

4、双向通道可以传递给参数为单向通道的函数

// 函数参数是单向通道
func sendMessage(in chan<- int) {
    for i := 0; i < 3; i++ {
        in <- i
    }
    close(in)
}
func main() {
    ch := make(chan int) // 双向通道
    go sendMessage(ch)
    for data := range ch {
        display(data)
    }
}

5、当读取通道与select搭配使用,并且设置了超时时间时,通道一定要设置缓冲

先看例子:

func sendMessage(in chan<- int, sleep time.Duration) {
    time.Sleep(sleep)
    in <- 1
}
func main() {
    display("开始")
    display("协程数量:", runtime.NumGoroutine())
    ch1 := make(chan int) // 错误
    // 正确:ch1 := make(chan int, 1)
    // 协程1
    go sendMessage(ch1, 5 * time.Second)
    select {
    case v := <-ch1:
        display("从通道1获取到了数据:", v)
    case <-time.After(1 * time.Second):
        display("超时了,退出select")
    }
    for {
        display("协程数量:", runtime.NumGoroutine())
        time.Sleep(1 * time.Second)
    }
}

如上面代码所示,一开始我们创建了一个无缓冲的通道ch1,然后开启【协程1】,【协程1】在 5 秒后会往通道写入一个数据,但select的超时时间只设置了 1 秒。也就是说,在【协程1】往通道写入数据前,select语句就已经因为超时而结束了,此时的ch1通道已经没有接收方,只剩下发送方了。往一个无缓冲的通道写入数据会导致【协程1】阻塞,而且没有了接收方,【协程1】就会永远阻塞下去,无法结束退出,从而导致协程泄露。

观察超时后打印出来的协程数量,一直都是2,不会降低为1,也证实了上面的说法。所以在定义通道变量时,一定要设置缓冲区。

其实调高 select的超时时间,也能解决这个问题。但有时候我们可能无法得知协程具体的执行耗时,从而预估出一个合理的超时时间,所以稳妥起见,还是定义一个带缓冲的通道比较好。

到此这篇关于golang使用通道时需要注意的一些问题的文章就介绍到这了,更多相关golang 通道内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • GoLang内存模型详细讲解

    GoLang内存模型详细讲解

    go官方介绍go内存模型的时候说:探究在什么条件下,goroutine 在读取一个变量的值的时,能够看到其它 goroutine 对这个变量进行的写的结果,Go内存模型规定了一些条件,在这些条件下,在一个goroutine中读取变量返回的值能够确保是另一个goroutine中对该变量写入的值
    2022-12-12
  • go语言处理TCP拆包/粘包的具体实现

    go语言处理TCP拆包/粘包的具体实现

    TCP的拆包/粘包也算是网络编程中一个比较基础的问题了,本文主要介绍了go语言处理TCP拆包/粘包,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-12-12
  • Go 代码块作用域变量遮蔽问题解析

    Go 代码块作用域变量遮蔽问题解析

    这篇文章主要为大家介绍了Go 代码块作用域变量遮蔽问题解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-10-10
  • golang sudog指的是什么

    golang sudog指的是什么

    sudog代表在等待队列中的goroutine,比如channel发送接受,由于goroutine和同步对象的关系是多对多,因此需要sudog映射,本文重点介绍golang sudog指的是什么,感兴趣的朋友一起看看吧
    2024-02-02
  • golang中http请求的context传递到异步任务的坑及解决

    golang中http请求的context传递到异步任务的坑及解决

    这篇文章主要介绍了golang中http请求的context传递到异步任务的坑及解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-03-03
  • Golang中Bit数组的实现方式

    Golang中Bit数组的实现方式

    这篇文章主要介绍了Golang中Bit数组的实现方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • golang 如何获取map所有key的方式

    golang 如何获取map所有key的方式

    这篇文章主要介绍了golang 获取map所有key的方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • 一文带你掌握Go语言中的文件读取操作

    一文带你掌握Go语言中的文件读取操作

    这篇文章主要和大家分享一下Go语言中的文件读取操作,文中的示例代码讲解详细,对我们学习Go语言有一定的帮助,需要的小伙伴可以参考一下
    2022-12-12
  • Golang实现自己的orm框架实例探索

    Golang实现自己的orm框架实例探索

    这篇文章主要为大家介绍了Golang实现自己的orm框架实例探索,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2024-01-01
  • GO语言基本类型分析

    GO语言基本类型分析

    这篇文章主要介绍了GO语言基本类型,较为详细的分析了整形、浮点型、字符串、指针等类型的具体用法,是深入学习GO语言所必须掌握的重要基础,需要的朋友可以参考下
    2014-12-12

最新评论