go nil channels

ddatsh

dev #go
package main

import "fmt"

func main() {
	a := make(chan int)
	b := make(chan int)
	go func() {
		for i := 0; i < 5; i++ {
			a <- i
		}
		close(a)
	}()
	go func() {
		for i := 0; i < 15; i++ {
			b <- i
		}
		close(b)
	}()
	for {
		select {
		case v := <-a:
			fmt.Println("receive from a", v)
		case v := <-b:
			fmt.Println("receive from b", v)
		}

	}
}

创建两个 goroutines,在 channels a b 上发送消息。发送完值后,关闭 channel

main routine无限循环来消耗channel中的值。不关心处理的顺序,使用 for..select处理来自channel的值

打印从通道获得的值后,无限循环继续运行。循环继续打印来自两个通道 b 和 a 的 0 值

一旦channel关闭,并且所有值都从其缓冲区中耗尽,通道将始终立即返回零值

receive from b 0
receive from b 1
receive from a 0
receive from a 1
receive from b 2
receive from b 3
receive from a 2
receive from a 3
receive from a 4
receive from a 0
receive from b 4
receive from b 5
receive from a 0
receive from b 6
receive from b 7
receive from a 0
receive from b 8
receive from a 0
receive from b 9
receive from a 0
receive from a 0
receive from a 0
receive from a 0
receive from b 10
receive from a 0
receive from b 11
receive from a 0
receive from b 12
receive from a 0
receive from a 0
receive from b 13
receive from a 0
receive from b 14
receive from a 0
receive from a 0
receive from b 0
receive from a 0
...
for {
		select {
		case v, ok := <-a:
			if !ok {
				fmt.Println("channel a closed")
				continue
			}
			fmt.Println("receive from a", v)
		case v, ok := <-b:
			if !ok {
				fmt.Println("channel b closed")
				continue
			}
			fmt.Println("receive from b", v)
		}
	}

通过从channel中获取开关ok值来了解通道何时关闭

虽然停止打印 0 值,但仍在关闭了的channel上无限迭代并打印相同的值

Channel a is closed
Channel a is closed
Channel b is closed
Channel b is closed
...

通过aClosed bClosed两个变量,每当知道channel关闭时,尝试打破循环

aClosed, bClosed := false, false
for !aClosed || !bClosed {
    select {
    case v, ok := <-a:
        if !ok {
            aClosed = true
            fmt.Println("channel a closed")
            continue
        }
        fmt.Println("receive from a", v)
    case v, ok := <-b:
        if !ok {
            bClosed = true
            fmt.Println("channel b closed")
            continue
        }
        fmt.Println("receive from b", v)
    }
}

能够不再在 for 循环中无限运行。但仍然会多次打印 Channel a is closed

receive from b 0
receive from a 0
receive from a 1
receive from b 1
receive from a 2
receive from a 3
receive from a 4
channel a closed
channel a closed
channel a closed
channel a closed
receive from b 2
channel a closed
channel a closed
receive from b 3
receive from b 4
channel a closed
channel a closed
channel a closed
receive from b 5
receive from b 6
channel a closed
receive from b 7
channel a closed
receive from b 8
receive from b 9
receive from b 10
channel a closed
channel a closed
channel a closed
channel a closed
channel a closed
channel a closed
receive from b 11
channel a closed
channel a closed
channel a closed
receive from b 12
channel a closed
channel a closed
channel a closed
receive from b 13
receive from b 14
channel b closed

当使用具有 select 多个channel操作的语句时,如果其中一个channel关闭,select 立即解析该channel,加上每次 select 都可能选择已关闭通道的case,*因此,看到 Channel a is closed 被打印了多次

为避免此问题,一旦通道关闭,应该从select中移除对应通道的case,由于select 无法动态移除case,可以通道设置 nil channel实现类似效果(go 从 nil channel接收或发送会永远阻塞,所以select 将不会选择那个case)

aClosed, bClosed := false, false
	for !aClosed || !bClosed {
		select {
		case v, ok := <-a:
			if !ok {
				aClosed = true
				a = nil
				fmt.Println("channel a closed")
				continue
			}
			fmt.Println("receive from a", v)
		case v, ok := <-b:
			if !ok {
				bClosed = true
				b = nil
				fmt.Println("channel b closed")
				continue
			}
			fmt.Println("receive from b", v)
		}
	}
receive from b 0
receive from b 1
receive from b 2
receive from a 0
receive from b 3
receive from a 1
receive from a 2
receive from b 4
receive from a 3
receive from b 5
receive from b 6
receive from a 4
receive from b 7
channel a closed
receive from b 8
receive from b 9
receive from b 10
receive from b 11
receive from b 12
receive from b 13
receive from b 14
channel b closed