当多个 goroutine 向同一通道发送数据时,需确保仅由一个协程在所有发送者完成后再关闭通道,否则会触发“send on closed channel” panic;推荐使用 sync.waitgroup 协调发送者生命周期,并在独立 goroutine 中执行 close()。
当多个 goroutine 向同一通道发送数据时,需确保仅由一个协程在所有发送者完成后再关闭通道,否则会触发“send on closed channel” panic;推荐使用 sync.waitgroup 协调发送者生命周期,并在独立 goroutine 中执行 close()。
在 Go 并发编程中,通道(channel)是核心通信机制,但其关闭规则极为严格:通道只能被关闭一次,且关闭后不可再发送数据。一旦违反(如多个 goroutine 竞争关闭、或关闭后仍有 goroutine 尝试发送),程序将立即 panic —— 这正是原代码的根本问题:10 个并发的 gen goroutine 都在各自循环末尾调用 close(ch),导致首个完成的 goroutine 关闭通道后,其余 goroutine 仍继续向已关闭通道写入,触发崩溃。
✅ 正确解法:WaitGroup + 单点关闭
关键原则是:关闭操作必须延迟到所有发送者 goroutine 确认退出后,且仅执行一次。sync.WaitGroup 是 Go 官方推荐的同步原语,用于等待一组 goroutine 完成:
- wg.Add(1) 在启动每个发送 goroutine 前调用,声明待等待任务数;
- defer wg.Done() 在发送 goroutine 内部延迟执行,确保无论正常结束还是 panic,都能准确通知完成;
- 主 goroutine 启动一个独立 goroutine 调用 wg.Wait() 并随后 close(ch),避免阻塞主流程;
- 接收端保持 for i := range ch 不变,该语法天然支持等待通道关闭并消费完所有已发送值。
以下是修正后的完整可运行代码:
package main
import (
"fmt"
"sync"
"time"
)
func gen(ch chan int, wg *sync.WaitGroup) {
defer wg.Done() // 确保 goroutine 结束时登记完成
for i := 0; ; i++ {
time.Sleep(time.Millisecond * 10)
select {
case ch <- i:
// 发送成功
default:
// 可选:非阻塞发送失败时优雅退出(如接收端已提前关闭)
return
}
if i >= 100 { // 注意:i > 100 会导致多发一次,应为 >= 100 或 i == 100
break
}
}
}
func receiver(ch chan int) {
for i := range ch {
fmt.Println("received:", i)
}
}
func main() {
ch := make(chan int)
var wg sync.WaitGroup
// 启动 10 个发送 goroutine
for i := 0; i < 10; i++ {
wg.Add(1)
go gen(ch, &wg)
}
// 在新 goroutine 中等待全部发送者完成,然后关闭通道
go func() {
wg.Wait()
close(ch)
}()
// 主 goroutine 执行接收逻辑(阻塞直到通道关闭)
receiver(ch)
}⚠️ 关键注意事项
- 关闭必须在非主 goroutine 中执行:若将 wg.Wait() 和 close(ch) 放入 main 函数直序执行,主 goroutine 会在关闭通道后立即退出,导致 receiver goroutine(若未在 main 中运行)被强制终止,部分数据丢失。本例中 receiver(ch) 在 main 中同步运行,因此能完整消费所有值。
- defer wg.Done() 是防御性设计:即使 gen 内部发生 panic,Done() 仍会被调用,防止 wg.Wait() 永久阻塞。
- 缓冲通道同样适用:该方案对 make(chan int, N) 也完全有效,无需修改逻辑。
- 边界条件检查:原示例中 if i > 100 会导致 i=101 时才退出,但 i=100 已被发送,实际生成了 101 个数(0~100)。建议改为 if i >= 100 或 if i == 100 以符合预期。
✅ 总结
关闭多生产者通道的黄金法则:谁创建,谁负责协调关闭;用 WaitGroup 计数,用独立 goroutine 执行 close。这不仅是解决 panic 的技术方案,更是 Go 并发模型中“明确责任归属”与“解耦生命周期”的典型实践。掌握此模式,可安全应用于数据库分页读取、事件流聚合、分布式任务分发等真实场景。


















