goroutine 启动下载任务没效果是因为主 goroutine 过早退出,需用 sync.WaitGroup 等待;并发过多易触发 429 或资源耗尽,应通过带缓冲 channel(如 sem := make(chan struct{}, 10))限流。

goroutine 启动下载任务时为什么没效果?
常见现象是写了 go downloadFile(url) 却发现所有请求串行发出,甚至只完成第一个。根本原因通常是主 goroutine 过早退出——Go 程序不会等待未显式同步的子 goroutine 结束。
必须用 sync.WaitGroup 或 channel 控制生命周期:
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
downloadFile(u) // 实际下载逻辑
}(url)
}
wg.Wait() // 阻塞直到全部完成
- 切记传参用
(url)而非(urls[i])闭包陷阱,否则所有 goroutine 可能共享最后一个url值 - 不要在循环里直接用
go downloadFile(url)+defer wg.Done(),因为defer在函数返回时才执行,而匿名函数已返回,Done()永远不调用
如何限制并发数避免被封或压垮服务?
无节制启 goroutine(比如 1000 个)会耗尽本地文件描述符、触发 HTTP 连接池瓶颈,或让目标服务器返回 429 Too Many Requests。需用带缓冲的 channel 做信号量控制:
sem := make(chan struct{}, 10) // 最多 10 个并发
for _, url := range urls {
sem <- struct{}{} // 获取令牌
go func(u string) {
defer func() { <-sem }() // 归还令牌
downloadFile(u)
}(url)
}
// 等待所有 goroutine 启动后,再等它们结束(需配合 WaitGroup)
- 缓冲大小不是越大越好:
http.DefaultClient默认只保持 100 个空闲连接,MaxIdleConnsPerHost默认 2,建议设为 5–20 之间并实测 - 别把
sem和WaitGroup混用逻辑:前者控并发,后者控完成,两者通常共存
downloadFile 函数里哪些地方容易阻塞主线程?
看似简单的 http.Get 其实暗藏多个阻塞点:DNS 解析、TCP 握手、TLS 握手、响应体读取。任一环节超时都会卡住整个 goroutine。
立即学习“go语言免费学习笔记(深入)”;
AJAX即“Asynchronous Javascript And XML”(异步JavaScript和XML),是指一种创建交互式网页应用的网页开发技术。它不是新的编程语言,而是一种使用现有标准的新方法,最大的优点是在不重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页内容,不需要任何浏览器插件,但需要用户允许JavaScript在浏览器上执行。《php中级教程之ajax技术》带你快速
必须显式设置超时:
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
},
}
resp, err := client.Get(url)
-
client.Timeout是总超时,但无法中断 DNS 查询;更细粒度要用DialContext和TLSHandshakeTimeout - 下载大文件时,
resp.Body的Read仍可能无限挂起,需用io.CopyN或带超时的io.ReadFull包裹 - 别忽略
resp.Body.Close(),否则连接无法复用,很快耗尽MaxIdleConns
如何安全地把下载结果写入文件而不冲突?
多个 goroutine 并发写同一个文件会导致内容错乱,但为每个文件开独立 goroutine 写入又可能触发系统级文件句柄上限。
推荐「下载与写入分离」:goroutine 只负责获取 []byte 或 io.ReadCloser,用 channel 发给单个 writer goroutine 统一落盘:
type DownloadResult struct {
URL string
Data []byte
Err error
}
results := make(chan DownloadResult, 100)
go func() {
for r := range results {
if r.Err != nil {
log.Printf("fail %s: %v", r.URL, r.Err)
continue
}
os.WriteFile(fileName(r.URL), r.Data, 0644)
}
}()
// 下载 goroutine 中:
results <- DownloadResult{URL: url, Data: data, Err: err}
- channel 缓冲区大小要权衡:太小会阻塞 downloader,太大吃内存;一般设为并发数 × 2~5
- 如果文件很大,别用
[]byte,改用io.ReadCloser+io.Copy流式写入,避免内存爆掉 - 注意文件名去重和路径安全,
url.Path直接拼接可能产生../路径穿越
实际跑起来会发现,瓶颈往往不在 goroutine 数量,而在 DNS 解析延迟、TLS 握手抖动、或磁盘 I/O 调度。真要压榨性能,得先用 pprof 定位哪一环在拖慢整体吞吐。









