最稳妥做法是用 sync.WaitGroup + 带缓冲 channel:启动前 wg.Add(n),每个 goroutine 结束时 defer wg.Done() 并发结果,另起 goroutine wg.Wait() 后 close(ch),主协程 for range ch 遍历,并用 select+time.After 防超时。

用 sync.WaitGroup + chan 收集结果最稳妥
直接用 channel 接收多个 goroutine 的输出,但不加同步控制,极容易漏数据或 panic。核心问题是:主 goroutine 不知道其他 goroutine 何时结束,close channel 的时机难把握,早了会丢结果,晚了可能死锁。
正确做法是组合 sync.WaitGroup 和带缓冲的 channel:
-
WaitGroup.Add(n)在启动 goroutine 前调用,确保计数准确 - 每个 goroutine 执行完后
defer wg.Done(),再把结果发到 channel(不阻塞) - 主 goroutine 启动所有任务后,另起一个 goroutine 调用
wg.Wait(),然后close(ch) - 主 goroutine 用
for range ch安全遍历,无需担心未关闭或提前关闭
select 配合 time.After 防止无限等待
实际场景中,某些 goroutine 可能卡住或超时,不能让整个聚合逻辑被拖住。纯 for range ch 会一直等,直到 channel 关闭 —— 但如果某个 goroutine panic 或死锁,WaitGroup 就永远等不到完成。
安全做法是在接收侧加超时控制:
立即学习“go语言免费学习笔记(深入)”;
for i := 0; i < n; i++ {
select {
case res := <-results:
// 处理 res
case <-time.After(5 * time.Second):
// 超时,可记录告警或补默认值
break
}
}注意:time.After 每次新建一个 timer,别在循环外复用;如果需要精确控制“最多等 5 秒收齐全部结果”,就用 context.WithTimeout 更合适。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
避免 channel 缓冲区大小误设导致阻塞或丢数据
声明 channel 时写成 make(chan int, 0)(无缓冲)看似“严格”,但一旦某个 goroutine 发送失败(比如接收端还没开始读),它就会阻塞甚至卡死整个流程。而写成 make(chan Result, 1000) 又可能掩盖并发逻辑缺陷 —— 比如本该并行 10 个任务,却因缓冲太大,实际只启了 1 个 goroutine 就填满了 channel。
推荐策略:
- 缓冲大小 = 预期最大结果数(比如你明确启动 8 个 goroutine,就设
make(chan T, 8)) - 如果结果数不确定,改用无缓冲 channel +
go func() { ch 匿名封装,把发送异步化(注意 closure 变量捕获问题) - 绝不要用
len(ch) == cap(ch)判断是否满 —— 这不是原子操作,且无法反映接收端进度
错误:在 goroutine 里直接向已关闭的 channel 发送
常见反模式是把 close(ch) 放在 wg.Wait() 后,但没考虑:万一某个 goroutine 还在执行 ch ,此时 channel 已关,就会 panic:<code>send on closed channel。
根本解法只有两个:
- 所有发送必须在
close(ch)之前完成 —— 所以wg.Wait()必须在发送 goroutine 全部退出后才调用(即defer wg.Done()必须在ch 之后) - 或者彻底放弃 close,改用带长度限制的接收循环(
for i := 0; i ),靠数量而非 channel 状态判断结束
后者更简单、更可控,尤其适合结果数量确定的场景,比如并发请求固定 N 个 API。

















