
在 go 中,应由生产者而非消费者负责关闭通道;消费者只需通过 range 遍历接收数据,而生产者应在所有数据发送完毕后显式调用 close(),这是符合 go 通道使用惯例的安全实践。
在并发编程中,使用通道(channel)收集多个 goroutine 的执行结果是一种常见模式。但一个关键原则是:通道的关闭责任必须明确归属于生产者(sender),而非消费者(receiver)。原代码中,消费者在 for v := range ch 循环内通过 len(ch) == 0 判断并主动关闭通道,这不仅逻辑脆弱(len(ch) 仅反映缓冲区当前长度,无法保证无新数据待写入),更违反了 Go 的通道契约——关闭未被发送方控制的通道可能导致 panic(如重复 close)或竞态行为。
正确的做法是将所有生产逻辑封装在一个 goroutine 内,由它协调子 goroutine 并统一关闭通道。如下重构后的代码清晰体现了这一职责分离:
package main
import (
"fmt"
"sync"
)
func main() {
ch := make(chan int, 3) // 缓冲大小设为 3,与任务数匹配,避免阻塞
var wg sync.WaitGroup
// 启动一个“协调协程”:启动所有生产者 + 等待完成 + 关闭通道
go func() {
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) 匹配预期发送数量,避免不必要的内存占用或潜在阻塞;
- ✅ 关闭权责分明:close(ch) 位于 wg.Wait() 之后,确保所有 goroutine 已完成发送;
- ✅ 消费者零耦合:range ch 自动在通道关闭后退出,无需手动检测或关闭逻辑;
- ⚠️ 注意闭包陷阱已修复:使用 func(val int) 参数捕获循环变量,避免所有 goroutine 共享同一个 i 值(原代码虽因传参 i 而幸免,但此写法更健壮、可读性更高)。
进阶建议:
对于更复杂的场景(如带错误处理、超时控制或动态任务数),可考虑使用 errgroup.Group 或 context.WithTimeout 进行增强,但核心原则不变:谁发送,谁关闭;谁接收,只遍历。遵循此模式,即可写出清晰、安全、符合 Go idioms 的并发收集代码。










