本文详解如何用go的goroutine安全、高效地并发下载多个url内容到本地文件,并重点解决因main函数提前退出导致goroutine未执行的经典陷阱。
本文详解如何用go的goroutine安全、高效地并发下载多个url内容到本地文件,并重点解决因main函数提前退出导致goroutine未执行的经典陷阱。
在Go中启动goroutine(如 go downloadFromUrl(url))是异步的——它立即返回,不阻塞主流程。因此,若main函数在goroutines完成前就结束,整个程序将直接退出,所有正在运行或尚未调度的goroutine都会被强制终止。这正是你遇到“函数看似没运行”的根本原因:downloadFromUrl其实已被调度,但main瞬间结束,进程终止,导致你无法观察到任何输出。
要让主协程等待所有下载任务完成,最标准、最推荐的方式是使用 sync.WaitGroup。它通过计数器机制协调goroutine生命周期:每启动一个goroutine就调用 Add(1),任务结束时调用 Done(),主协程通过 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() // 确保Done()在函数退出时执行
// 解析文件名:取URL路径最后一段,避免非法字符
tokens := strings.Split(url, "/")
filename := tokens[len(tokens)-1]
if filename == "" || filename == "/" {
filename = "index.html"
}
// 清理文件名(简单处理,生产环境建议更严格校验)
filename = filepath.Base(filename)
// 发起HTTP请求
resp, err := http.Get(url)
if err != nil {
log.Printf("Failed to fetch %s: %v", url, err)
return
}
defer resp.Body.Close()
// 创建本地文件
file, err := os.Create(filename)
if err != nil {
log.Printf("Failed to create file %s: %v", filename, err)
return
}
defer file.Close()
// 流式写入,避免内存溢出
_, err = io.Copy(file, resp.Body)
if err != nil {
log.Printf("Failed to write to %s: %v", filename, err)
return
}
fmt.Printf("✅ Downloaded %s → %s\n", url, filename)
}
func main() {
urls := []string{
"https://httpbin.org/html",
"https://httpbin.org/json",
"https://httpbin.org/robots.txt",
}
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go downloadFromUrl(url, &wg)
}
wg.Wait() // 主协程在此阻塞,直到所有下载完成
fmt.Println("? All downloads completed.")
}关键注意事项:
立即学习“go语言免费学习笔记(深入)”;
- ✅ *必须传递 `sync.WaitGroup指针**:WaitGroup内部维护计数器,需通过指针修改;传值会导致每个goroutine操作副本,Wait()` 将永远阻塞。
- ✅ defer wg.Done() 是最佳实践:确保无论函数如何退出(正常/panic/错误),计数都能正确减少。
- ⚠️ 避免闭包变量捕获陷阱:本例中直接传参 url,而非在循环内 go func(){...}(url),彻底规避了经典“所有goroutine共享最后一个url值”的bug。
- ?️ 错误处理不可省略:网络请求和文件I/O极易失败,务必检查 err 并记录,否则静默失败难以调试。
- ? 大文件请务必流式处理:使用 io.Copy 而非 ioutil.ReadAll,防止内存爆炸。
总结:goroutine本身没有问题,问题在于缺少同步机制。sync.WaitGroup 是Go并发编程的基石工具之一,掌握它,才能真正驾驭并发——既发挥并行优势,又保障程序可靠性。


















