最稳妥方式是用 sync.waitgroup 配合带缓冲的 chan:启动前声明容量等于任务数的 chan,waitgroup.add() 在 goroutine 启动前调用,每个 goroutine 以 defer wg.done() 确保完成通知,主协程通过 for range chan 消费结果,避免共享 slice 或 map 导致竞态。

用 sync.WaitGroup + chan 组合最稳妥
直接上结论:别用全局变量或临时切片存结果,也别在 goroutine 里直接 append 到共享 slice——那是竞态高发区。正确姿势是启动 goroutine 前先声明一个带缓冲的 chan(容量等于任务数),再配合 sync.WaitGroup 控制生命周期。
常见错误现象:fatal error: concurrent map writes 或结果漏掉、重复、顺序错乱。根本原因是多个 goroutine 同时写同一 slice 或 map,而 Go 不保证写操作原子性。
- 缓冲通道大小建议设为
len(tasks),避免 goroutine 阻塞等待接收方 -
WaitGroup.Add()必须在 goroutine 启动前调用,不能放 inside goroutine - 每个 goroutine 执行完必须调用
defer wg.Done(),且要确保它一定会被执行(比如别被 panic 跳过) - 主 goroutine 用
go func() { ... }()启动收集协程,或用for range chan消费,别用for i := 0; i 硬等
为什么不用 sync.Map 或普通 map 存结果
sync.Map 是为高频读+低频写的场景设计的,写性能远低于 channel 发送;而普通 map 在并发写时直接 panic。如果你只是收集几十到几百个结果,channel 的内存拷贝开销几乎可忽略,但安全性和可读性碾压所有“加锁 map”方案。
使用场景很明确:结果类型统一、数量可控、无需实时响应中间状态。比如批量调用 API、并行校验文件、聚合数据库查询结果。
在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。
- 若结果结构体较大,考虑传指针进 channel(
chan *Result),减少拷贝 - 若需保留原始执行顺序,别依赖 channel 接收顺序——goroutine 启动顺序 ≠ 完成顺序。可在结果结构体里加
index int字段,最后按 index 排序 - 不要用
map[int]Result配sync.RWMutex,代码膨胀且易漏锁
超时控制和错误聚合怎么加
纯 WaitGroup + chan 不处理失败或超时。真实项目必须加一层封装:用 context.WithTimeout 包裹每个子任务,并让每个 goroutine 把 error 也发到同一个结果 channel(定义为 chan ResultOrError 类型)。
错误信息容易踩的坑:nil error 不能直接 send 进非空接口 channel,会导致接收方无法区分“成功”和“没返回”。必须统一用 struct 封装:
type ResultOrError struct {
Result *MyResult
Err error
Index int
}
- 主 goroutine 在
select中监听ctx.Done(),及时关闭 channel 并 return - 每个子 goroutine 开始前检查
ctx.Err() != nil,避免无效启动 - 收集阶段用
for i := 0; i 配合 <code>timeoutselect,防止永远阻塞
要不要用第三方库比如 errgroup
golang.org/x/sync/errgroup 确实省事,但它默认不支持结果收集——只管错误传播和取消。你要的结果数组还得自己配 channel 或 mutex。所以它适合“只要全成功就 OK,否则立刻失败”的场景(如初始化服务依赖),不适合“尽量多拿结果,汇总统计”的需求。
性能影响很小,但引入额外依赖会让简单逻辑变重。50 行以内的并发收集,手写 WaitGroup + chan 更透明、更易调试。
- 如果已有
errgroup且任务都返回相同类型,可以 wrap 成func() (interface{}, error),再用 map 存结果,但要注意并发写 map 的风险依然存在 - 别为了“优雅”强行套用泛型 collector 库——Go 的 channel 语义足够清晰,过度抽象反而掩盖了 goroutine 生命周期的真实约束
WaitGroup 的 Add/Done 位置严格守规矩、错误和结果走同一通道并带上下文标识。其余都是围绕这三点的变形。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










