2. 使用 sync.WaitGroup 等待一组 goroutine 完成

使用 sync.WaitGroup 等待一组 goroutine 完成 学习卡片

为什么需要 WaitGroup

例 1 中主 goroutine 一旦执行到 main 末尾就会退出,其他 goroutine 会被强制终止。当我们需要“等一组并发任务全部完成再继续”时,sync.WaitGroup 就是最直接的工具。它内部维护一个计数器:Add(n) 增加待完成任务数,每个任务结束时调用 Done() 把计数减 1,Wait() 会阻塞直到计数归零。

WaitGroup 计数器驱动主 goroutine 等待 main goroutine wg.Add(3) 启动 3 个 goroutine wg.Wait() 阻塞 计数=0 后继续 打印汇总并退出 WaitGroup 计数器 3 → 2 → 1 → 0 Add / Done 原子更新 goroutine #1 下载 URL-A → Done() goroutine #2 下载 URL-B → Done() goroutine #3 下载 URL-C → Done() Add(3) 计数归零唤醒 Done()

并发下载多个 URL 的实现

下面程序并发下载 3 个 URL,每个 goroutine 完成后调用 Done(),主 goroutine 用 Wait() 等待全部结束再打印汇总。为便于本地复现,示例使用 http.Get 请求公开测试地址,并只读取响应状态码与内容长度。

package main

import (
	"fmt"
	"io"
	"net/http"
	"sync"
	"time"
)

type result struct {
	url    string
	status int
	length int
	err    error
}

func download(url string, wg *sync.WaitGroup, out chan<- result) {
	defer wg.Done() // 任务结束,计数减 1

	client := http.Client{Timeout: 5 * time.Second}
	resp, err := client.Get(url)
	if err != nil {
		out <- result{url: url, err: err}
		return
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		out <- result{url: url, status: resp.StatusCode, err: err}
		return
	}
	out <- result{url: url, status: resp.StatusCode, length: len(body)}
}

func main() {
	urls := []string{
		"https://example.com",
		"https://httpbin.org/get",
		"https://www.golang.org",
	}

	var wg sync.WaitGroup
	out := make(chan result, len(urls))

	wg.Add(len(urls)) // 先登记任务数,再启动 goroutine
	for _, u := range urls {
		go download(u, &wg, out)
	}

	wg.Wait() // 阻塞直到计数器归零
	close(out)

	fmt.Println("全部下载完成,汇总如下:")
	for r := range out {
		if r.err != nil {
			fmt.Printf("  %-28s 失败: %v\n", r.url, r.err)
			continue
		}
		fmt.Printf("  %-28s 状态=%d 长度=%d 字节\n", r.url, r.status, r.length)
	}
}
关键点:wg.Add(n) 必须在启动 goroutine 之前调用。若在 goroutine 内部调用 Add,可能出现 Wait() 先于 Add 执行、计数仍为 0 而提前返回的竞态。

运行与结果判读

执行 go run main.go,预期输出形如:

全部下载完成,汇总如下:
  https://example.com           状态=200 长度=1256 字节
  https://httpbin.org/get       状态=200 长度=312 字节
  https://www.golang.org        状态=200 长度=... 字节

判读要点:

  • 三行结果全部打印,说明 Wait() 确实等到了所有 goroutine 结束,主程序没有提前退出。
  • 输出顺序与 urls 顺序不一定一致,因为 goroutine 调度与网络耗时不同,这正是并发特征。
  • 若某行显示“失败”,说明该 URL 请求出错,但其他任务仍会继续执行,WaitGroup 只负责计数同步,不负责错误传播。
常见错误:把 wg.Add(1) 写在 go download(...) 之后,或忘记 defer wg.Done(),都会导致 Wait() 永久阻塞或提前返回。使用 defer 能保证即使函数中途 return,计数也会被正确减 1。

可复现检查清单

检查项正确做法错误后果
Add 时机启动 goroutine 前调用 wg.Add(len(urls))Wait 可能提前返回
Done 调用每个 goroutine 内 defer wg.Done()计数无法归零,永久阻塞
Wait 位置所有 goroutine 启动后调用 wg.Wait()主程序提前退出
结果收集用带缓冲 channel 或互斥锁保护共享数据数据竞态或丢失结果