本文详解 `sync.waitgroup` 常见误用导致程序卡在 `wg.wait()` 不返回的问题,重点说明值传递 vs 指针传递、`defer wg.done()` 的调用时机等关键陷阱,并提供可立即修复的代码示例。
在 Go 并发编程中,sync.WaitGroup 是协调多个 goroutine 完成等待的经典工具。但若使用不当,极易引发程序“假死”——看似所有任务已完成,wg.Wait() 却永不返回。你提供的代码正是典型反例,问题根源在于两个关键错误:
函数 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() 写在函数末尾(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
}
旦 Wait() 返回,该 WaitGroup 实例不应再被 Add() 或再次 Wait();如需复用,请重新声明变量。 修复后的完整 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 程序。