
go 程序中启动 goroutine 后主函数立即退出,导致协程未执行即终止;需用 sync.waitgroup 等待所有下载任务完成,才能确保并发下载正确执行。
go 程序中启动 goroutine 后主函数立即退出,导致协程未执行即终止;需用 sync.waitgroup 等待所有下载任务完成,才能确保并发下载正确执行。
在 Go 中利用 Goroutine 实现并发下载是典型的应用场景,但新手常因忽略程序生命周期而遇到“协程没运行就退出”的问题。根本原因在于:main() 函数执行完毕后整个程序立即终止,而 go downloadFromUrl(url) 启动的协程尚未来得及调度或完成——即使只是打印日志,也会因主线程提前退出而丢失输出。
✅ 正确做法:用 sync.WaitGroup 同步协程生命周期
WaitGroup 是 Go 标准库提供的轻量级同步原语,用于等待一组 Goroutine 完成。其核心方法有三个:
- Add(n):增加待等待的协程计数;
- Done():表示一个协程已完成(通常配合 defer 使用);
- Wait():阻塞直到计数归零。
以下是完整、可运行的并发下载示例(含真实 HTTP 下载逻辑):
package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
)
func downloadFromUrl(url string, wg *sync.WaitGroup) {
defer wg.Done() // 确保无论成功失败都计数减一
// 生成安全的文件名(避免路径遍历 & 特殊字符)
tokens := strings.Split(url, "/")
filename := tokens[len(tokens)-1]
if filename == "" || filename == "/" {
filename = "index.html"
}
// 清理非法文件名字符(简化版,生产环境建议用更健壮方案)
filename = filepath.Base(filename)
fmt.Printf("⬇️ 开始下载: %s → %s\n", url, filename)
resp, err := http.Get(url)
if err != nil {
log.Printf("❌ 请求失败 %s: %v", url, err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Printf("❌ HTTP %d: %s", resp.StatusCode, url)
return
}
outFile, err := os.Create(filename)
if err != nil {
log.Printf("❌ 创建文件失败 %s: %v", filename, err)
return
}
defer outFile.Close()
_, err = io.Copy(outFile, resp.Body)
if err != nil {
log.Printf("❌ 写入文件失败 %s: %v", filename, err)
return
}
fmt.Printf("✅ 已保存: %s\n", filename)
}
func main() {
urls := []string{
"https://httpbin.org/html",
"https://httpbin.org/json",
"https://httpbin.org/xml",
}
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go downloadFromUrl(url, &wg)
}
wg.Wait() // ⚠️ 关键:阻塞 main,等待全部下载完成
fmt.Println("? 所有下载任务已完成!")
}
? 注意事项与最佳实践
- 不要在循环中直接传 url 的地址:若使用 &url(如 go f(&url)),因 url 是循环变量,所有协程可能共享同一内存地址,导致竞态读取错误。本例通过值传递 url string,天然安全。
- 错误处理不可省略:HTTP 请求、文件创建、IO 写入均可能失败,务必逐层检查 err 并合理记录。
- 资源及时释放:使用 defer 关闭 resp.Body 和 outFile,防止句柄泄漏。
- 文件名安全:直接截取 URL 最后一段可能产生非法路径(如 ../etc/passwd),示例中使用 filepath.Base() 做基础防护,生产环境建议结合正则或专用库(如 slugify)进一步净化。
- 限流与超时:真实项目中应为 http.Client 设置 Timeout,并考虑用 semaphore 或 context.WithTimeout 控制并发数,避免对目标服务器造成压力。
掌握 WaitGroup 是 Go 并发编程的基石。它不涉及 Channel 的复杂通信逻辑,专为“等待 N 个任务结束”而设计——简洁、高效、零内存分配,是本场景最直接、最推荐的解决方案。











