Skip to content

Channel 基础操作

面试题:对已经关闭的 chan 进行读写,会怎么样?为什么?

问题分析

这是一个关于 Channel 生命周期的核心面试题,考察对 Channel 关闭后行为的深入理解。

测试代码

点击查看完整代码实现
点击查看完整代码实现
go
package main

import (
    "fmt"
    "time"
)

func testClosedChannelOperations() {
    // 创建一个缓冲为 2 的 channel
    ch := make(chan int, 2)
    
    // 发送一些数据
    ch <- 1
    ch <- 2
    
    // 关闭 channel
    close(ch)
    fmt.Println("Channel 已关闭")
    
    // 测试读操作
    testRead(ch)
    
    // 测试写操作
    testWrite(ch)
}

func testRead(ch chan int) {
    fmt.Println("\n=== 测试关闭的 Channel 读操作 ===")
    
    // 读取剩余数据
    for i := 0; i < 4; i++ {
        value, ok := <-ch
        fmt.Printf("第%d次读取: value=%d, ok=%t\n", i+1, value, ok)
    }
    
    // 使用 range 读取
    fmt.Println("\n使用 range 读取(重新创建 channel):")
    ch2 := make(chan int, 2)
    ch2 <- 10
    ch2 <- 20
    close(ch2)
    
    for value := range ch2 {
        fmt.Printf("range 读取: %d\n", value)
    }
    fmt.Println("range 循环结束")
}

func testWrite(ch chan int) {
    fmt.Println("\n=== 测试关闭的 Channel 写操作 ===")
    
    // 使用 defer + recover 捕获 panic
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("捕获到 panic: %v\n", r)
        }
    }()
    
    // 尝试向已关闭的 channel 写入数据
    ch <- 3  // 这里会 panic
}

func main() {
    testClosedChannelOperations()
}

:::

运行结果

点击查看完整代码实现
点击查看完整代码实现
Channel 已关闭

=== 测试关闭的 Channel 读操作 ===
第1次读取: value=1, ok=true
第2次读取: value=2, ok=true
第3次读取: value=0, ok=false
第4次读取: value=0, ok=false

使用 range 读取(重新创建 channel):
range 读取: 10
range 读取: 20
range 循环结束

=== 测试关闭的 Channel 写操作 ===
捕获到 panic: send on closed channel

:::

行为详解

1. 对已关闭 Channel 的读操作

不会 panic,但行为有所不同:

  • 有缓冲数据时:正常返回数据和 true
  • 无缓冲数据时:立即返回零值和 false
  • 持续读取:每次都返回零值和 false
go
ch := make(chan int, 2)
ch <- 1
close(ch)

v1, ok1 := <-ch  // v1=1, ok1=true (读取缓冲数据)
v2, ok2 := <-ch  // v2=0, ok2=false (无数据,返回零值)
v3, ok3 := <-ch  // v3=0, ok3=false (仍然返回零值)

2. 对已关闭 Channel 的写操作

会立即 panic:

go
ch := make(chan int)
close(ch)
ch <- 1  // panic: send on closed channel

深入原理

Channel 的内部状态

go
type hchan struct {
    qcount   uint           // 当前队列中的元素个数
    dataqsiz uint           // 环形队列的大小
    buf      unsafe.Pointer // 环形队列的指针
    closed   uint32         // 关闭标志
    // ... 其他字段
}

读操作的源码逻辑

点击查看完整代码实现
点击查看完整代码实现
go
// 简化的接收逻辑
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    // 检查是否有缓冲数据
    if c.qcount > 0 {
        // 从缓冲队列中取数据
        return true, true
    }
    
    // 检查 channel 是否关闭
    if c.closed != 0 {
        // 已关闭且无数据,返回零值
        return true, false
    }
    
    // 其他逻辑...
}

:::

写操作的源码逻辑

go
// 简化的发送逻辑
func chansend(c *hchan, ep unsafe.Pointer, block bool) bool {
    // 首先检查是否关闭
    if c.closed != 0 {
        panic("send on closed channel")
    }
    
    // 其他发送逻辑...
}

实际应用场景

1. 优雅关闭模式

点击查看完整代码实现
点击查看完整代码实现
go
func worker(done chan bool, tasks chan Task) {
    for {
        select {
        case task, ok := <-tasks:
            if !ok {
                // tasks channel 已关闭,退出工作循环
                done <- true
                return
            }
            // 处理任务
            processTask(task)
        }
    }
}

// 主函数中
close(tasks)  // 通知所有 worker 停止工作

:::

2. 扇出模式(Fan-out)

点击查看完整代码实现
点击查看完整代码实现
go
func fanOut(input chan int, workers int) []chan int {
    outputs := make([]chan int, workers)
    
    for i := 0; i < workers; i++ {
        outputs[i] = make(chan int)
        go func(out chan int) {
            defer close(out)  // 确保输出 channel 被关闭
            for value := range input {  // input 关闭时自动退出
                out <- value * 2
            }
        }(outputs[i])
    }
    
    return outputs
}

:::

3. 安全的发送函数

go
func safeSend(ch chan int, value int) bool {
    defer func() {
        recover()  // 忽略 panic
    }()
    
    select {
    case ch <- value:
        return true
    default:
        return false
    }
}

面试注意事项

1. 零值的含义

不同类型的零值:

  • int: 0
  • string: ""
  • bool: false
  • pointer: nil
  • slice: nil

2. 检查 Channel 状态的方法

go
// 方法1:使用第二个返回值
value, ok := <-ch
if !ok {
    // channel 已关闭
}

// 方法2:使用 range(推荐)
for value := range ch {
    // 处理数据
}
// range 会在 channel 关闭时自动退出

3. 关闭 Channel 的最佳实践

go
// 1. 只有发送方应该关闭 channel
// 2. 不要在接收方关闭 channel
// 3. 可以使用 sync.Once 确保只关闭一次
var once sync.Once
once.Do(func() {
    close(ch)
})

总结

操作未关闭 Channel已关闭 Channel
读取(有数据)正常返回数据和 true正常返回数据和 true
读取(无数据)阻塞等待立即返回零值和 false
写入正常写入或阻塞panic
关闭正常关闭panic

关键要点:

  1. 关闭的 Channel 读取不会 panic,写入会 panic
  2. 关闭的 Channel 可以继续读取缓冲数据
  3. 读取完缓冲数据后,返回对应类型的零值和 false
  4. 使用 range 循环是处理 Channel 关闭的最佳方式

正在精进