
本文详解 `sync.waitgroup` 常见误用导致程序卡在 `wg.wait()` 不返回的问题,重点说明值传递 vs 指针传递、`defer wg.done()` 的调用时机等关键陷阱,并提供可立即修复的代码示例。
在 Go 并发编程中,sync.WaitGroup 是协调多个 goroutine 完成等待的经典工具。但若使用不当,极易引发程序“假死”——看似所有任务已完成,wg.Wait() 却永不返回。你提供的代码正是典型反例,问题根源在于两个关键错误:
? 错误一:WaitGroup 被值传递(copy),导致 Done() 失效
函数 downloadFromURL(url string, wg sync.WaitGroup) 的第二个参数是 值类型,Go 会复制整个 WaitGroup 结构体传入。后续在 goroutine 中调用 wg.Done(),实际操作的是副本,对 main 中原始 wg 的计数器 零影响。因此 wg.Wait() 永远等待未完成的 goroutine。
✅ 正确做法:必须传递指针
go downloadFromURL(url, &wg) // 传地址
并同步更新函数签名:
func downloadFromURL(url string, wg *sync.WaitGroup) error {
// ...
}? 错误二:defer wg.Done() 放置位置不当
当前代码中 defer wg.Done() 写在函数末尾(return nil 之前),看似合理,实则危险:一旦函数因错误提前 return(如文件创建失败、HTTP 请求异常),defer 将被跳过,Done() 永不执行,WaitGroup 计数器无法归零。
✅ 正确做法:defer wg.Done() 应置于函数最开始
它应是 goroutine 启动后立即注册的“收尾承诺”,确保无论函数以何种路径退出,计数器必减一:
func downloadFromURL(url string, wg *sync.WaitGroup) error {
defer wg.Done() // ✅ 第一行就声明:我结束时必调用 Done()
tokens := strings.Split(url, "/")
fileName := tokens[len(tokens)-1]
fmt.Printf("Downloading %v to %v \n", url, fileName)
content, err := os.Create("temp_docs/" + fileName)
if err != nil {
fmt.Printf("Error while creating %v because of %v\n", fileName, err)
return err // 此处 return → defer wg.Done() 仍会执行
}
defer content.Close() // 别忘了关闭文件!
resp, err := http.Get(url)
if err != nil {
fmt.Printf("Could not fetch %v because %v\n", url, err)
return err // 同样,defer wg.Done() 保证执行
}
defer resp.Body.Close()
_, err = io.Copy(content, resp.Body)
if err != nil {
fmt.Printf("Error while saving %v from %v\n", fileName, url)
return err
}
fmt.Printf("Download complete for %v \n", fileName)
return nil
}⚠️ 其他重要注意事项
- wg.Add(1) 必须在 go 语句前调用:你的代码已正确(wg.Add(1) 在 go downloadFromURL(...) 之前),这是安全的;若放在 goroutine 内部,则存在竞态风险。
- 避免重复 Add/Over-add:确保每个 go 启动的 goroutine 对应且仅对应一次 Add(1)。
- WaitGroup 不可复制或重用:一旦 Wait() 返回,该 WaitGroup 实例不应再被 Add() 或再次 Wait();如需复用,请重新声明变量。
- 调试技巧:sync.WaitGroup 本身不提供公开 API 查询当前计数,但可通过 unsafe 或 reflect(不推荐生产环境)窥探;更实用的方法是添加日志:在 Add(1) 和 Done() 处打印计数变化,或使用 go tool trace 分析 goroutine 生命周期。
修复后的完整 main 函数逻辑清晰、健壮可靠:
func main() {
links := parseLinks()
var wg sync.WaitGroup
for _, url := range links {
if isExcelDocument(url) {
wg.Add(1)
go downloadFromURL(url, &wg) // ✅ 传指针
} else {
fmt.Printf("Skipping: %v\n", url)
}
}
wg.Wait() // ✅ 现在能正确返回
fmt.Println("All downloads completed.")
}遵循这两条铁律——指针传递 WaitGroup + defer Done() 置顶——即可彻底规避 WaitGroup 永不完成的陷阱,写出稳定、可维护的并发 Go 程序。










