应使用带缓冲channel或semaphore限流,并配context超时、显式关闭resp.body、原子更新进度、errgroup统一错误处理。

并发下载函数怎么写才不卡死内存?
直接用 go downloadFile(...) 启动几百个协程,大概率触发 OOM 或被服务端限流。核心矛盾不是“能不能并发”,而是“如何控制并发数 + 避免 goroutine 泄漏”。必须显式限制最大并发数,并为每个下载任务设置超时和错误处理。
- 用
semaphore(信号量)或带缓冲的 channel 控制并发数,比如make(chan struct{}, 10)作为协程池令牌 - 每个下载任务必须有独立的
context.WithTimeout,避免单个失败阻塞整个池 - 不要在 goroutine 里直接调用
http.Get后裸奔,要检查resp.Body.Close()是否执行 —— 忘关会导致连接泄漏,最终dial tcp: too many open files - 文件写入建议用
io.CopyN或分块io.Copy,别一次性读进内存;大文件尤其要注意
进度统计为什么总不准?关键在原子操作和回调时机
常见错误是多个 goroutine 直接读写同一个 int 变量,导致统计值跳变或停滞。进度不是“已下载字节数”,而是“已成功写入磁盘的字节数”——必须在 os.File.Write 返回后才更新。
- 用
sync/atomic操作int64类型的总进度变量,例如atomic.AddInt64(&totalDownloaded, int64(n)) - 避免在 goroutine 内频繁调用回调函数(如打印进度),可加简单节流:每 100ms 或每 1MB 更新一次 UI
- 如果下载 URL 带重定向,
http.Client默认跟随,但resp.ContentLength可能为 -1(chunked),此时无法预估总大小,进度条只能显示“已下载 / unknown”
协程池要不要自己手写?用 golang.org/x/sync/errgroup 更稳
手写带错误传播、等待、取消的协程池容易漏边界条件。官方 errgroup 已覆盖绝大多数场景,且与 context 深度集成。
- 初始化:
g, ctx := errgroup.WithContext(context.Background()) - 启动任务:
g.Go(func() error { ... }),任意一个返回 error,其余会收到ctx.Err() - 限制并发数需配合信号量:在
Go函数内先sem ,退出前 <code> - 注意:不要在
g.Go外部提前 cancel ctx,否则所有任务立即中断 —— 应该让下载逻辑自己决定是否响应 cancel
实际代码里最容易漏的三件事
不是语法错,而是运行时悄无声息崩掉的点。
-
http.DefaultClient的Transport没调优:默认MaxIdleConnsPerHost = 2,并发 10 下载时大量连接排队,改成100更合理 - 临时文件没清理:下载中途 panic,
os.CreateTemp创建的文件残留,要用defer os.Remove(tempPath)+ 成功后os.Rename - URL 中含中文或特殊字符没
url.PathEscape:直接拼接导致Parse: invalid URL escape错误
并发下载真正的复杂点不在启动多少 goroutine,而在资源生命周期管理 —— 连接、文件句柄、内存缓冲、上下文取消,每一环断掉都会让程序行为不可预测。写完先跑 100 个链接压测 5 分钟,看 goroutine 数和内存是否平稳回落,比单元测试更能暴露问题。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











