最稳的聚合起点是 sync.WaitGroup + channel;需避免 wg.Wait() 后直接读全局 slice/map,因并发写会触发 fatal error 或数据丢失,应采用生产者-消费者模式,用缓冲大小等于任务总数的 channel 实现安全聚合。

sync.WaitGroup + channel 是最稳的聚合起点
别指望 wg.Wait() 后直接读全局 slice 或 map —— 并发写 []T 或 map[K]V 会触发 fatal error: concurrent map writes 或数据丢失。根本原因不是 Go 不支持并发,而是语言层明确禁止未加锁的共享写入。
真正安全的聚合结构是「生产者-消费者」解耦:每个 goroutine 只负责计算+发送,一个单独 goroutine 控制 channel 关闭时机,主协程只读不写。
-
results := make(chan Result, len(tasks)):缓冲大小必须等于任务总数,否则 sender 可能阻塞在results - 每个 worker 开头就
defer wg.Done(),哪怕fetch()panic 也不会卡死wg.Wait() - 关闭 channel 的 goroutine 必须在
wg.Wait()之后立刻close(results),不能放主协程里同步调用
Result 结构体必须带 error 字段
用 panic 替代错误返回是聚合逻辑中最常见的翻车点。一旦某个 goroutine panic,defer wg.Done() 虽然能执行,但主协程收结果时无法区分“失败”和“没来得及返回”,更没法做重试或降级。
结构体定义要显式携带错误状态:
立即学习“go语言免费学习笔记(深入)”;
type Result struct {
Data string
Err error
}- 所有 worker 都统一走
results ,不分支、不跳过 - 主协程收结果时用
if r.Err != nil分流处理,而不是靠 recover 捕获 panic - 避免在 goroutine 内部 log.Fatal 或 os.Exit,这会让整个程序退出,不是单个任务失败
超时控制不能只靠 for range ch
for range ch 会一直阻塞,直到 channel 关闭。但如果某个 goroutine 卡在 DNS 解析、慢 SQL 或死循环里,wg.Wait() 永远不会返回,close(results) 就永远不会执行,主协程就永远等下去。
必须在接收侧加主动超时:
- 用
select+time.After(5 * time.Second)做兜底,注意time.After每次都要新建,不能复用 - 更推荐
context.WithTimeout,尤其当需要向下游传递取消信号(比如 HTTP handler 超时) - 不要在 for 循环外提前 close channel —— 这会导致部分结果被丢弃,且
range会立即退出
任务数动态时缓冲通道大小不能硬编码
当任务来自 channel、数据库游标或流式输入时,你根本不知道总数。此时 make(chan Result, N) 的 N 就成了隐患:设小了可能阻塞 sender,设大了浪费内存,设 0 又退化成无缓冲 channel,容易死锁。
两种务实解法:
- 预估上限(比如最多 1000 个 API 请求),按上限建缓冲 channel,比全程无缓冲更可控
- 改用无缓冲 channel +
sync.Mutex保护一个全局 slice,仅当结果需保序或后续要多次遍历时才考虑(性能略低,但逻辑直白)
缓冲通道不是银弹,它解决的是“发送不阻塞”,但生命周期管理、错误传播、超时响应这些事,还得靠组合 WaitGroup、context 和清晰的 Result 定义来兜住。漏掉任何一环,都可能在线上安静地丢数据。


















