golang使用通道时需要注意的一些问题
作者:自由de单车
环境
- 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 通道内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!