在 go 中,sync.waitgroup.wait() 是阻塞调用,不支持原生超时;可通过协程封装为通道操作,结合 select 实现带超时的等待逻辑。
在 go 中,sync.waitgroup.wait() 是阻塞调用,不支持原生超时;可通过协程封装为通道操作,结合 select 实现带超时的等待逻辑。
sync.WaitGroup 是 Go 中实现“屏障(barrier)”语义的核心工具,常用于等待一组 goroutine 并发执行完毕(例如批量 HTTP 请求、数据预加载等场景)。但其 Wait() 方法是同步阻塞的,
无法直接设置超时
——这在生产环境中存在明显风险:若某个 goroutine 意外卡死或响应延迟,整个主流程将无限挂起。
幸运的是,Go 的并发模型天然支持“将阻塞操作转为非阻塞通道操作”。只需启动一个轻量 goroutine 执行 wg.Wait(),并在完成后关闭或发送信号到通道,即可将其接入 select 语句,轻松集成超时控制。
以下是一个完整、安全、可复用的实现示例:
?
关键注意事项
:
✅ 始终使用 close(done) 而非 done <- struct{}{}:前者更语义清晰,且避免 channel 未被接收时的 goroutine 泄漏;
⚠️ WaitGroup 超时后,
不会自动终止已启动的 goroutine
—— Go 不提供强制取消原语,需配合 context.Context 实现可取消的任务(如 http.Client 设置 Context);
? 若需多次等待同一 WaitGroup,请勿复用;每个 WaitGroup 应遵循“一次 Add/Wait 周期”,重复使用前需确保已重置(通常应新建);
? 进阶建议:可封装为可复用函数,例如 WaitWithTimeout(wg *sync.WaitGroup, timeout time.Duration) (bool, error),返回是否超时。
综上,虽然 sync.WaitGroup 本身无超时能力,但借助 Go 的 goroutine + channel 组合,我们能以极简、符合 Go 风格的方式优雅补全这一能力——这正是 Go “用通信共享内存”哲学的典型体现。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
done := make(chan struct{})
// 启动若干模拟任务(如并发 fetch URL)
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// 模拟可能耗时或失败的操作
duration := time.Duration(id+1) * 200 * time.Millisecond
time.Sleep(duration)
fmt.Printf("Task %d completed after %v\n", id, duration)
}(i)
}
// 将 Wait() 异步化为通道操作
go func() {
wg.Wait()
close(done) // 使用 close 而非 send,避免 channel 内存泄漏
}()
// 主协程通过 select 等待完成或超时
select {
case <-done:
fmt.Println("✅ All tasks finished successfully.")
case <-time.After(500 * time.Millisecond):
fmt.Println("⚠️ Timeout reached. Some tasks may still be running.")
// 注意:此处 wg 未完成,但主逻辑可继续执行(如降级、告警、取消等)
}
}