(2/26)GO练习题-nil channel技巧

nil channel 的高级用法:动态禁用 select case

踩坑故事

某团队实现了一个多路数据合并服务,从 A、B 两个数据源读取数据合并输出。 需求是:当某个数据源完成(已消费完所有数据)后,不要阻塞,继续处理另一个。 第一版代码在数据源完成后,channel 被关闭了,但 select 中仍然 case <-ch,每次读到零值。 事后大佬 review 说:把 channel 设成 nil 就好了。这个技巧让全组震惊。

核心知识点

nil channel 在 select 中的行为是永久阻塞,这个"缺点"反过来是一个强大的高级技巧:

select {
case v := <-ch1:  // 处理数据
case v := <-ch2:  // 处理数据
}

当 ch1 完成(被关闭)后,如果继续 select ch1,会不断读到零值。 解法:ch1 完成后,把 ch1 = nil。 nil channel 在 select 中的 case 永远无法触发, 相当于动态地从 select 中移除了这个 case。

问题描述

实现一个 merge 函数,从两个 channel 读取数据并合并输出到 result channel:

func merge(done <-chan struct{}, ch1, ch2 <-chan int) <-chan int

要求:

  1. 从 ch1 和 ch2 读取数据,交替输出到 result channel
  2. 当某个 channel 关闭后,自动从 select 中移除它(用 nil channel 技巧)
  3. 两个 channel 都关闭后,关闭 result channel
  4. 支持 done channel 取消

示例

ch1 := asChan(1, 2, 3)
ch2 := asChan(4, 5, 6)
for v := range merge(nil, ch1, ch2) {
    fmt.Println(v)  // 输出 1-6,顺序不定
}

面试追问

Q1: 为什么不用 ok 模式判断 channel 是否关闭?

:用 v, ok := <-ch 可以判断关闭,但 select 中每次读到关闭的 channel 都会返回零值, 需要一个循环不断消耗。nil channel 更优雅——永久阻塞等于移除了这个 case。

Q2: close(ch) 后还能从 ch 读到值吗?

:能!close 后 channel 不再阻塞,会不断返回零值。v, ok := <-ch 的 ok 为 false。

Q3: 什么场景需要动态禁用 select case?

:多源数据合并、graceful shutdown 等待多个 worker、动态路由等。

 

请补充代码实现

package main

import "fmt"

// asChan 将整数列表转换为 channel(方便测试)
func asChan(vals ...int) <-chan int {
	ch := make(chan int)
	go func() {
		for _, v := range vals {
			ch <- v
		}
		close(ch)
	}()
	return ch
}

// TODO: 实现 merge 函数
// 从 ch1 和 ch2 读取整数并合并输出到返回的 channel
// 当某个 channel 关闭后,应该停止监听它(而不是不断读到零值)
// 两个 channel 都关闭后,关闭返回的 channel
func merge(ch1, ch2 <-chan int) <-chan int {
	// TODO: 实现
	return nil
}

func main() {
	ch1 := asChan(1, 2, 3)
	ch2 := asChan(4, 5, 6)

	// 期望输出: 1 2 3 4 5 6(顺序可能交错)
	for v := range merge(ch1, ch2) {
		fmt.Println(v)
	}
	fmt.Println("全部完成")
}

 

posted @ 2026-06-21 22:12  夏至小茗  阅读(1)  评论(0)    收藏  举报