本文详解如何正确启动并等待多个 goroutine 并发下载网页内容,重点解决因主函数提前退出导致协程未执行的问题,并提供完整、健壮的下载实现。
本文详解如何正确启动并等待多个 goroutine 并发下载网页内容,重点解决因主函数提前退出导致协程未执行的问题,并提供完整、健壮的下载实现。
在 Go 中使用 go 关键字启动协程(goroutine)时,若不进行同步控制,程序很可能在协程真正执行前就已退出——这正是你遇到问题的根本原因:main() 函数执行完循环后立即结束,而所有 downloadFromUrl 协程尚未开始或中途被终止。
✅ 正确做法:用 sync.WaitGroup 等待所有协程完成
sync.WaitGroup 是 Go 标准库中专为协程同步设计的轻量工具,通过计数器管理协程生命周期:
- wg.Add(1) 在启动协程前增加计数;
- defer wg.Done() 在协程结尾自动减计数;
- wg.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"
}
// 清理非法文件名字符(简化版,生产环境建议用更严格的 sanitization)
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 错误 %s: status %d", url, resp.StatusCode)
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)
}
fmt.Println("⏳ 正在并发下载...")
wg.Wait() // 主线程在此阻塞,等待所有协程完成
fmt.Println("? 所有下载已完成!")
}
⚠️ 注意事项与最佳实践
- 不要在循环中直接传 &url:for _, url := range urls 中的 url 是每次迭代的副本,若在 goroutine 中直接引用 &url 会导致所有协程读取到同一个(最后)值。本例通过参数传值规避此问题。
- 错误处理不可省略:HTTP 请求、文件 I/O 均可能失败,务必检查 err 并合理日志记录。
- 资源及时释放:使用 defer resp.Body.Close() 和 defer outFile.Close() 避免句柄泄漏。
- 文件名安全:直接使用 URL 片段作文件名存在风险(如 ../etc/passwd),示例中使用 filepath.Base() 做基础防护,生产环境建议结合正则或专用库(如 slug)进一步净化。
- 限流与超时:高并发下载时应添加 http.Client 超时和连接池限制,防止耗尽系统资源或触发目标站反爬机制。
掌握 WaitGroup 是 Go 并发编程的基石能力。它不依赖 channel,简洁高效,适用于“启动一批任务并等待全部结束”的典型场景——正如批量下载、数据预热、并行计算等实际需求。











