sync.waitgroup 不收集返回值,并发写 slice/map 会导致 panic;应使用带缓冲 channel 由单独协程收集结果,并在 wg.wait() 后关闭 channel。

sync.WaitGroup 本身不收集返回值,别指望 wg.Wait() 后直接读全局变量
你写完一堆 go func() { ... results = append(results, x) }(),再 wg.Wait(),结果不是丢数据就是 panic —— 因为多个 goroutine 并发写 slice 或 map 是未定义行为,Go 运行时可能直接报 fatal error: concurrent map writes。
真正安全的聚合起点,是明确“谁负责写、谁负责读、谁控制生命周期”。
用带缓冲 chan + 单独关闭协程收结果
这是最常用也最不容易翻车的模式,适用于任务数已知、结果结构固定、不要求顺序的场景(比如求和、计数、拼接日志)。
- 缓冲通道大小设为任务总数:
make(chan Result, len(tasks)),避免 sender 协程因阻塞而卡住 -
wg.Wait()必须在新协程里调用并紧接着close(ch),否则主协程for range ch会永远等待 - 每个 worker 开头就
defer wg.Done(),防止fetch()panic 导致 wg 永远等不到完成 - 如果
fetch()可能失败,结果结构体要带Err error字段,别用 panic 替代错误传递
示例关键片段:
results := make(chan Result, len(urls))
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
r, err := fetchFromURL(u)
results <hr><h3>需要保留输入顺序?别依赖 channel 收到顺序</h3><p><code>chan</code> 不保证发送顺序与接收顺序一致(尤其当 worker 执行时间差异大时),更别说 Go 调度器本就不承诺 goroutine 启动/完成顺序。</p>
- 若必须按原始索引顺序聚合(如第 3 个 URL 的结果必须填到
output[2]),每个 worker 发送时带上索引:results ,主协程用预分配切片接收后排序或直接填位 - 或改用
sync.Mutex+ 预分配[]Result:worker 计算完直接mu.Lock(); results[i] = r; mu.Unlock(),省去 channel 通信开销(万级以下任务更稳)
注意:用 mutex 写切片时,务必提前 make([]Result, len(tasks)),否则 append 触发底层数组扩容会引发竞态。
并发数失控时内存爆炸?加信号量限流
不加限制地为每个 URL 启一个 goroutine,在几千个任务下可能瞬间创建上万个 goroutine,吃光内存或触发调度抖动。
- 用带缓冲的限流 channel:
sem := make(chan struct{}, 10),表示最多 10 个并发 - 每个 worker 启动前先
sem ,完成后 <code> - 把
sem当作资源令牌,比用runtime.GOMAXPROCS()更可控
示例:
sem := make(chan struct{}, 10)
for i, url := range urls {
sem <hr><p>聚合逻辑本身不是瓶颈,真正容易被忽略的是:<strong>channel 关闭时机不对会导致主协程永久阻塞;worker panic 会跳过 <code>wg.Done()</code>;限流没做导致 OOM;错误被静默吞掉而不是透出。</strong><br>
这些点不出问题时一切正常,一出就是线上事故。</p>golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











