17370845950

如何在Golang中管理多个并发任务_Golang WaitGroup与channel方法
WaitGroup 必须在启动 goroutine 前调用 Add(),否则可能 panic;需确保 Add() 与 Done() 配对,不可重复使用;动态任务需即时 Add();避免循环变量地址传递;用带缓冲 channel 控制并发数。

WaitGroup 必须在启动 goroutine 前 Add,否则可能 panic

常见错误是把 wg.Add(1) 放在 goroutine 内部,导致 Wait() 等不到任何任务,或 Done() 调用次数超过 Add(),触发 panic:「sync: negative WaitGroup counter」。

正确做法是在启动 goroutine 之前调用 Add(),确保计数器已就绪:

var wg sync.WaitGroup
for _, task := range tasks {
    wg.Add(1) // ✅ 必须在 go 前
    go func(t string) {
        defer wg.Done() // ✅ 必须配对
        process(t)
    }(task)
}
wg.Wait()
  • 如果任务数动态变化(如从 channel 拉取),需在每次拉取后立即 wg.Add(1)
  • WaitGroup 不是锁,不能重复使用(即不能在 Wait() 后再次 Add()),除非重置为零值(不推荐)
  • 避免在循环中传入循环变量地址,要用闭包参数传值,否则所有 goroutine 共享同一个变量

用 channel 控制并发数量,避免 Goroutine 泛滥

直接为每个任务启一个 goroutine 容易耗尽系统资源。用带缓冲的 channel 当“信号量”可限制最大并发数,比手动维护计数器更清晰可靠。

典型模式:一个 channel 作为令牌池,每启动一个任务先从 channel 取一个 token,完成后放回:

sem := make(chan struct{}, 5) // 最多 5 个并发
for _, task := range tasks {
    sem <- struct{}{} // 阻塞直到有空位
    go func(t string) {
        defer func() { <-sem }() // ✅ 保证释放
        process(t)
    }(task)
}
  • 缓冲大小即最大并发数,适合 I/O 密集型任务(如 HTTP 请求、文件读写)
  • 不要用 close(sem),它会导致后续 立即返回零值,破坏语义
  • 若需等待全部完成,仍需配合 WaitGroup 或额外 done channel

WaitGroup + channel 混合使用:收集结果并控制完成时机

单纯用 WaitGroup 无法安全收集返回值;只用 channel 可能因缓冲不足阻塞发送。两者结合最常用:W

aitGroup 管生命周期,channel 收结果。

关键点是结果 channel 应无缓冲或足够大,且所有 goroutine 统一发送后才关闭:

results := make(chan string, len(tasks))
var wg sync.WaitGroup
for _, task := range tasks {
    wg.Add(1)
    go func(t string) {
        defer wg.Done()
        res := process(t)
        results <- res // ✅ 发送不阻塞(缓冲足够)
    }(task)
}
go func() {
    wg.Wait()
    close(results) // ✅ 所有发送完成后关闭
}()
// 主协程接收
for res := range results {
    fmt.Println(res)
}
  • 结果 channel 缓冲大小建议设为 len(tasks),避免 sender 阻塞影响调度
  • 绝不能在 goroutine 内直接 close(results),会 panic:「close of closed channel」
  • 若任务可能 panic,需在 goroutine 内 recover,否则 Done() 不会被调用,Wait() 永不返回

别忽略 context.Context:超时与取消必须由上层驱动

WaitGroup 和 channel 本身不感知超时或取消。实际项目中,必须用 context.Context 包裹任务,否则单个卡死任务会让整个流程 hang 住。

正确姿势是将 ctx 传入每个 goroutine,并在 I/O 或循环中定期检查:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
for _, task := range tasks {
    wg.Add(1)
    go func(t string) {
        defer wg.Done()
        select {
        case <-ctx.Done():
            log.Println("canceled:", t)
            return
        default:
            processWithContext(ctx, t) // 在内部检查 ctx.Err()
        }
    }(task)
}
wg.Wait()
  • 不要在 goroutine 中调用 cancel() —— 只有发起方才能决定取消时机
  • HTTP client、database query、time.Sleep 等都支持 ctx 参数,优先使用它们的上下文版本
  • WaitGroup 不提供 cancel 能力,这是 context 的职责,二者分工明确

WaitGroup 和 channel 各有边界:前者管“是否做完”,后者管“如何通信与限流”。真正难的是在复杂流程中保持 context 传递、错误归并、以及 panic 恢复的统一处理——这些不会自动发生,得每一层都主动考虑。