必须用 sync.WaitGroup 显式跟踪并发请求完成状态:启动前 wg.Add(1),结束后 wg.Done(),主协程 wg.Wait() 等待;避免 wg.Add 在协程内、漏调 wg.Done、复用未重置 WaitGroup;结果通过带缓冲 channel 安全收集,结构体封装响应与错误;用信号量 channel 控制并发数。

用 sync.WaitGroup 控制并发请求生命周期
直接起一堆 goroutine 调用接口却不等它们结束,结果切片永远是空的——这是最常见错误。必须用 sync.WaitGroup 显式跟踪每个请求的完成状态。
关键点:在启动 goroutine 前调用 wg.Add(1),在 goroutine 结束时调用 wg.Done();主 goroutine 调用 wg.Wait() 阻塞等待全部完成。
容易踩的坑:
-
wg.Add()必须在 goroutine 启动前调用,不能放在 goroutine 内部 - 如果请求失败后提前 return,忘了调用
wg.Done(),会导致wg.Wait()永久阻塞 - 不要复用同一个
WaitGroup实例做多次不同批次的请求,未重置会 panic
用 chan 安全收集结果(带错误处理)
多个 goroutine 并发写入同一 slice 会引发 data race,必须通过 channel 中转结果。推荐定义结构体封装响应和错误:
type Result struct {
Data interface{}
Err error
ID string // 可选,用于标识来源请求
}使用带缓冲的 channel(如 make(chan Result, len(urls)))避免 goroutine 阻塞,主 goroutine 在 wg.Wait() 后关闭 channel,再 range 读取所有结果。
注意:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 不要在 goroutine 内直接向无缓冲 channel 发送——若主 goroutine 还没开始接收,会死锁
- 即使某个请求失败(
Err != nil),也要把Result{Err: err}发送到 channel,否则结果数量对不上 - 如果需要保持请求顺序,channel 不保证顺序,得靠
ID字段后续排序
控制并发数:用带缓冲的 semaphore 代替无限制 goroutine
对同一服务发起上百个并发请求,很容易触发限流或连接耗尽。Go 没有内置信号量,但可用带缓冲的 channel 模拟:
sem := make(chan struct{}, 10) // 最多 10 个并发每个请求前先 sem 占位,结束后 <code> 释放。这比 <code>time.Sleep 或粗暴的 runtime.GOMAXPROCS 更精准可控。
典型误用:
- 把
sem声明在 for 循环内,每次新建一个 channel,完全失去限流效果 - 忘记在 defer 或 recover 中释放信号量,导致后续请求永久卡住
- 缓冲大小设为 0(即无缓冲 channel),变成串行执行,失去并发意义
超时与重试要分层设置:context.WithTimeout + client.Timeout
单个 HTTP 请求超时和整批请求总超时必须分开控制。前者由 http.Client.Timeout 或 http.NewRequestWithContext 处理,后者用 context.WithTimeout 包裹整个批量逻辑。
示例场景:
- 单请求最多 3 秒,整批最多 15 秒 → 即使某请求卡住,其他仍可继续,且整体不会拖太久
- 重试应只针对临时性错误(如
net.OpError、HTTP 5xx),非幂等操作(如 POST)不应自动重试 - 别用
time.AfterFunc手动 cancel,优先用context.CancelFunc,更易传递和组合
真正难处理的是部分请求成功、部分失败后的聚合逻辑——比如 10 个请求里 7 个返回了数据、2 个超时、1 个 401,这时候怎么返回?得按业务定策略,不是加个 channel 就完事的。

















