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...)
}
为了减少代码冗余,下面的代码例子就不再贴出此函数的代码了。
怎么理解这句话呢,首先要了解读写通道时什么情况下会阻塞:
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
循环读取完通道的数据后,会自动结束循环,不会阻塞在读取通道处,所以不会报错。
这句话告诉我们,当发送方不再需要发送数据时,可以关闭通道,但不能让接收方去关闭。
因为接收方并不知道发送方是否还需要发送数据,如果胡乱关闭了通道,会导致发送方触发panic
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)
}*/
}
// 函数参数是单向通道
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)
}
}
先看例子:
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
的超时时间,也能解决这个问题。但有时候我们可能无法得知协程具体的执行耗时,从而预估出一个合理的超时时间,所以稳妥起见,还是定义一个带缓冲的通道比较好。