最稳妥的多channel合并方案是sync.WaitGroup+for range:每个输入channel启独立goroutine拉取数据,wg等待全部退出后关闭输出channel;需显式传参捕获channel、输出channel无缓冲、defer wg.Done()置于for外层。

用 sync.WaitGroup + for range 合并多个 channel 最稳妥
直接在主 goroutine 里用 select 轮询多个未关闭的 chan int,几乎必然导致死锁或漏数据。真正能落地的方案是:每个输入 channel 单独起一个 goroutine,内部用 for range 拉完即退,再靠 sync.WaitGroup 统一等待、关闭输出 channel。
关键点:
-
go func(c chan int)必须显式传参捕获当前ch,否则闭包共享循环变量,所有 goroutine 实际读同一个 channel - 输出 channel 不要带缓冲(
make(chan int)),避免因缓冲满而阻塞 sender 导致 goroutine 泄漏 -
defer wg.Done()放在for range外层,确保即使 channel 永不关闭,goroutine 也能被主动 cancel(配合 context 更好)
固定两个 channel 时,select + nil channel 技巧够用但有局限
如果确定只有两个输入 channel,且生命周期可控(比如都由同一方关闭),可以用 select 配合将已关闭 channel 置为 nil 的手法。它比 WaitGroup 轻量,但无法扩展到动态数量,也不适合存在长期未关闭 channel 的场景。
典型写法:
立即学习“go语言免费学习笔记(深入)”;
func merge(ch1, ch2 <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for ch1 != nil || ch2 != nil {
select {
case v, ok := <-ch1:
if !ok {
ch1 = nil
} else {
out <- v
}
case v, ok := <-ch2:
if !ok {
ch2 = nil
} else {
out <- v
}
}
}
}()
return out
}注意:ch1 和 ch2 类型必须是 <-chan int,不能是双向 channel;for 循环条件不能省略,否则 select 在全 nil 时会 panic
用 reflect.Select 动态合并任意数量 channel,但代价高
当输入 channel 数量不固定(比如从配置加载、运行时生成),reflect.Select 是唯一标准库方案。但它需要构造 []reflect.SelectCase,每次调用都有反射开销,且无法静态检查 channel 类型是否一致。
适用场景有限:
- channel 数量极少(≤ 5)、吞吐不高(如配置监听、信号聚合)
- 无法提前知道 channel 列表,又不愿引入第三方库
- 不介意每次
reflect.Select带来的微小延迟和 GC 压力
别把它当成通用替代品——90% 的业务场景,WaitGroup 方案更清晰、更易 debug。
合并后 channel 关闭时机不对,会导致 for range 提前退出或死锁
输出 channel 的关闭,必须严格发生在「所有输入 goroutine 全部退出之后」。早关 → 主 goroutine for range 提前结束,漏数据;晚关 → 主 goroutine 一直阻塞,或依赖超时兜底。
常见错误:
- 在启动所有 goroutine 后立刻
close(out)—— 完全没等数据进来 - 把
wg.Wait()放在主 goroutine 里同步调用 —— 阻塞住,无法并发接收 - 用
defer close(out)包在 goroutine 函数体里 —— 每个 goroutine 都关一次,panic: close of closed channel
正确姿势:另起一个 goroutine 调用 wg.Wait(),然后 close(out)。这个 goroutine 和转发 goroutine 是平级的,谁都不等谁。


















