头图

Go 面试:channel 关闭后为什么还能读到值?worker 怎么退出

我见过一个 worker 停不下来的小事故。上游已经执行了 close(jobs),日志却还在不断打印 0。排查到最后不是 goroutine 没收到关闭信号,而是接收端把“读到零值”当成了一份真任务。

close 不会把缓冲区里的数据清掉。它只是宣布以后不能再发送。已经排队的数据照样会被接收端取走,直到队列真的空了。

Go channel 关闭后的接收时序

拿一个容量为 2 的 channel 跑一下就很直观:

package main

import "fmt"

func main() {
    ch := make(chan int, 2)
    ch <- 7
    ch <- 9
    close(ch)

    for {
        v, ok := <-ch
        fmt.Printf("v=%d, ok=%v\n", v, ok)

        if !ok {
            return
        }

        fmt.Println("handle:", v)
    }
}

输出会是:

v=7, ok=true
handle: 7
v=9, ok=true
handle: 9
v=0, ok=false

前两次是缓冲区里原本就有的 7 和 9。第三次才是关闭后的接收结果:int 的零值 0,配合 ok=false。

很多人只写成这样:

for {
    job := <-jobs
    handle(job)
}

它不会因为 channel 关闭而退出。关闭后的接收立即返回零值,于是这个循环会拿着 0 一直跑。要是 job 是指针,得到的是 nil;要是是结构体,拿到的则是一份零值结构体。问题通常不会立刻 panic,反而更难发现。

worker 只是消费一批任务时,我更常用 range:

for job := range jobs {
    handle(job)
}

它会把缓冲区里剩下的任务消费完,然后自然结束。只有当代码确实要区分“收到的值”和“channel 已关闭”时,才展开写 v, ok := <-ch。

面试里再追问一句“谁来 close”,别答谁方便谁关。通常由发送方、或者明确拥有发送权的一侧关闭。接收方不知道后面还有没有生产者;随手关掉,很容易让仍在发送的 goroutine 直接 panic。


威武的凉茶
1 声望0 粉丝