
在 go 中并发收集任务结果时,应由生产者而非消费者负责关闭通道;本文详解正确的通道关闭模式、常见误区及最佳实践。
在 Go 的并发编程中,使用通道(channel)协调生产者与消费者是常见模式,但谁该关闭通道是一个关键设计决策。原始代码中,消费者在 for range ch 循环内检测 len(ch) == 0 并主动 close(ch),这种做法虽能运行,但违背了 Go 的通道语义原则:只有明确知道所有发送操作已完成的生产者,才有权关闭通道。否则易引发 panic(如向已关闭通道发送数据)或竞态逻辑(如多个 goroutine 争抢关闭)。
正确的做法是将所有生产逻辑封装在单一 goroutine 内,由它统一管理 sync.WaitGroup 并在所有任务完成后再调用 close(ch)。这样既保证了关闭时机的确定性,又使消费者职责纯粹——仅消费,不干预生命周期。
以下是优化后的标准实现:
package main
import (
"fmt"
"sync"
)
func main() {
ch := make(chan int, 3) // 缓冲容量匹配预期发送数量,避免阻塞
var wg sync.WaitGroup
// 启动一个“生产协调器” goroutine
go func() {
// 启动 3 个并发生产者
for i := 0; i < 3; i++ {
wg.Add(1)
go func(val int) {
defer wg.Done()
ch <- val // 发送结果
}(i)
}
wg.Wait() // 等待所有生产者完成
close(ch) // ✅ 安全关闭:确保无更多发送
}()
// 消费者:简洁、健壮地遍历
for v := range ch {
fmt.Println("consume", v)
}
fmt.Println("All done")
}⚠️ 关键注意事项:
- 缓冲区大小建议设为预期结果数(如 make(chan int, 3)):避免生产者因通道满而阻塞,同时防止内存浪费;
- 切勿在消费者中关闭通道:range 会自动在通道关闭后退出,无需手动检查 len(ch);
- 禁止重复关闭通道:Go 运行时会 panic,因此必须确保仅由单一生产者关闭;
- 若需错误处理或超时控制,可结合 select + context 增强鲁棒性。
此模式清晰分离关注点:生产协调器负责启动、同步与终止;消费者专注处理数据流。它是 Go 并发编程中收集多任务结果的惯用且安全的标准范式。










