2. 使用 sync.WaitGroup 等待一组 goroutine 完成
为什么需要 WaitGroup
例 1 中主 goroutine 一旦执行到 main 末尾就会退出,其他 goroutine 会被强制终止。当我们需要“等一组并发任务全部完成再继续”时,sync.WaitGroup 就是最直接的工具。它内部维护一个计数器:Add(n) 增加待完成任务数,每个任务结束时调用 Done() 把计数减 1,Wait() 会阻塞直到计数归零。
并发下载多个 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 或互斥锁保护共享数据 | 数据竞态或丢失结果 |