如何安全关闭多个 goroutine 共用的 Go 通道
如何安全关闭多个 goroutine 共用的 Go 通道
在 Go 的并发世界里,通道(channel)是协程间通信的基石,好用但“脾气”不小。它有一条铁律:一个通道只能被关闭一次,而且关闭之后,任何发送操作都会立刻引发 panic。这就像一扇门,只能由一个人来上锁,锁上之后谁也别想再往里推东西,否则门框都得晃三晃。
免费影视、动漫、音乐、游戏、小说资源长期稳定更新! 👉 点此立即查看 👈
文章开头那段代码的症结就在于此:10个并发的 `gen` 协程,每个都在干完活后试图去关同一扇门。结果是,手最快的那个协程把门关上了,后面姗姗来迟的几位却还想着往里塞数据,程序不崩溃才怪。

✅ 正确解法:WaitGroup + 单点关闭
那么,正确的姿势是什么?核心原则就两条:关闭操作必须等到所有“发送者”都确认退场之后才能进行,并且这个动作只能发生一次。
实现这个目标,Go 标准库里的 `sync.WaitGroup` 是绝佳搭档。它的工作模式很清晰:
- 每启动一个发送协程前,用 `wg.Add(1)` 登记一下,告诉 WaitGroup:“又多了一个人等会儿需要你关照”。
- 在每个发送协程的内部,用 `defer wg.Done()` 确保无论这个协程是正常结束还是中途“翻车”(panic),都会在退出时举手报告:“我这边完事了”。
- 然后,我们启动一个独立的、专门负责关门的协程。它啥也不干,就调用 `wg.Wait()` 安静地等着,直到所有登记在册的发送协程都报告“Done”了,它才从容地执行 `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)
}
⚠️ 关键注意事项
方案虽好,但魔鬼藏在细节里。实施时,有几个坑点需要特别留意:
- 关门这事,最好别让主协程干:如果把 `wg.Wait()` 和 `close(ch)` 直接放在 `main` 函数里顺序执行,主协程在关闭通道后会立刻退出。如果接收协程(比如 `receiver`)不是在主协程中同步运行的,就可能被强行终止,导致部分数据“胎死腹中”。好在上面代码里,`receiver(ch)` 是同步调用的,所以能稳稳地收完所有数据。
- `defer wg.Done()` 是道保险:这是一种防御性编程。即使 `gen` 函数内部发生了意想不到的 panic,`defer` 语句也能保证 `Done()` 被调用,从而避免 `wg.Wait()` 永远等不到人,造成程序死锁。
- 缓冲通道照用不误:这个方案对带缓冲的通道(`make(chan int, N)`)同样有效,逻辑完全一样,无需任何调整。
- 边界条件要抠细:回头看看原示例里的 `if i > 100`,这个条件会导致循环在 `i` 变成 101 时才跳出,但此时 `i=100` 已经被发送出去了。这意味着实际会生成 0 到 100 共 101 个数。通常我们的意图是发送 100 个,所以建议改为 `if i >= 100` 或者 `if i == 100`。
✅ 总结
处理多生产者通道的关闭问题,可以总结为一条黄金法则:创建者负责协调,用 WaitGroup 清点人数,用独立协程执行关闭。这不仅仅是一个避免 panic 的技术技巧,更是 Go 并发哲学“责任明确”和“生命周期解耦”的生动体现。熟练掌握这个模式,无论是数据库的分页读取、多个事件流的聚合,还是分布式任务的分发与收集,你都能处理得游刃有余。
相关攻略
如何实现一个支持过期时间的 LRU 缓存(Go 实现)? 先说一个核心结论:Go 标准库的 container list 本身并不具备过期能力,你必须自己动手,组合定时清理或惰性检查机制。直接套用 sync Map 加上独立的定时器,这条路走不通,很容易导致数据漏删或者重复触发,可靠性堪忧。 为什么
MongoDB 3 6旧版本如何平滑迁移GridFS数据 在MongoDB 3 6版本中,使用mongodump进行数据备份时,默认会忽略GridFS存储所使用的fs files和fs chunks集合,因为它们被系统视为内部命名空间。为确保GridFS文件数据的完整迁移,必须显式指定导出这两个集合
如何在低带宽环境下高效同步MongoDB副本集数据 初始化同步流量激增的根源:未压缩的oplog全量传输 许多数据库管理员在向MongoDB副本集添加新节点时,都会遭遇网络流量飙升的困扰。监控显示带宽被长时间占满,同步过程可能持续数日。这一问题的核心症结在于MongoDB的initial sync(
MongoDB 7 0环境下如何管理GridFS元数据:在fs files集合中自定义属性 为什么直接往 fs files 插入文档会失败 在MongoDB 7 0中,如果你尝试绕过标准API,直接向fs files集合插入文档,大概率会碰壁。原因很简单:fs files并非一个普通的集合,它是Gr
深入解析MongoDB DBRef:引用机制详解与手动引用实战对比 DBRef 本质解析:它并非自动关联,而是携带元数据的指针 许多MongoDB开发者在初次接触DBRef时,常误以为它能实现类似SQL JOIN的自动关联查询。实际上,无论是MongoDB原生驱动、Node js环境、Python的
热门专题
热门推荐
一、授予系统权限并启动基础服务 想让BetterTouchTool真正“活”起来,第一步就得打通系统权限。它需要“辅助功能”权限来监听你的触控板事件,也需要“屏幕录制”权限来执行一些窗口操作。这两项权限缺一不可,否则你会发现手势做了,但电脑毫无反应。 具体操作其实不复杂:先进入系统「设置」-「隐私与
如何开启Windows 11“高性能模式” 解决笔记本玩游戏掉帧降频方法 笔记本玩游戏,最扫兴的莫过于画面突然卡顿、帧率断崖式下跌。很多时候,问题并非出在硬件本身,而是Windows 11默认的电源策略在“拖后腿”。为了省电,系统会动态调节处理器频率、让核心休眠,甚至给显卡设置功耗墙,这直接限制了硬
macOS更新失败?别慌,这五步能帮你搞定 升级macOS时,进度条卡住不动、弹窗提示“无法验证更新”或者干脆报错退出,这事儿确实让人头疼。其实,这些看似随机的故障,背后通常逃不出几个核心原因:存储空间不连续、网络连接不干净、缓存文件有冲突,或者磁盘底层出了点小状况。别担心,按照下面这套经过验证的步
Linux下使用Jattach工具诊断Ja va进程 零停机获取Dump信息 开门见山,先说一个核心判断:jattach 并非 JDK 自带工具,也不能直接替代 jstack。但它的价值在于,能在某些棘手场景下,绕过 JVM 的安全限制成功获取 dump。当然,这有个前提——目标 JVM 的 Att
Tyk Dashboard 启动失败?从配置到排查的完整指南 在Linux上部署Tyk,可不是简单的apt install或yum install就能搞定。它背后依赖着MongoDB和Redis,并且对配置顺序有严格的要求。跳过其中任何一环,tyk-dashboard服务很可能就会卡在502错误,或





