应使用带缓冲的channel控制并发数,如sem := make(chan struct{}, 10),每次下载前

goroutine 泄漏导致内存爆满怎么办
不加限制地起 go downloadImage(url),几万张图下来进程直接 OOM。根本原因不是下载慢,是没等下载完协程就挂了,但 HTTP 连接、响应体缓冲、甚至未回收的 http.Client 实例还在占内存。
- 必须用带缓冲的 channel 控制并发数,比如
sem := make(chan struct{}, 10),每次下载前sem ,结束后 <code> - 别复用全局
http.Client时忽略Timeout和Transport.MaxIdleConnsPerHost,否则 DNS 缓存+连接池会悄悄吃光句柄 - 用
context.WithTimeout(ctx, 30*time.Second)包裹每个请求,防止某张图卡死拖垮整个池
文件名冲突和路径安全怎么处理
直接用 URL 的 path.Base() 当文件名,遇到 https://i.example.com/1?w=800&h=600 就生成 1?w=800&h=600,Linux 下创建失败,Windows 直接报错 invalid argument。
- 用
url.Parse()解析后取u.Path,再用path.Base(u.Path);接着用strings.TrimSuffix()去掉查询参数干扰 - 对结果做一次
filepath.Clean()防止../路径穿越,再用正则替换非法字符:re := regexp.MustCompile(`[:"/\|?*]`)→re.ReplaceAllString(name, "_") - 保存前先
os.MkdirAll(dir, 0755),别假设目录一定存在
下载失败后如何重试又不阻塞池子
单次失败就 return,会导致任务丢失;全量重试又可能把超时请求反复塞进池子,压垮目标站或自己。
- 每张图最多重试 2 次,用
for i := 0; i 包裹核心逻辑,成功就 <code>break - 每次重试前加
time.Sleep(time.Second (即 1s、2s、4s),避免雪崩式重试 - 失败日志里必须打上原始
url和err.Error(),否则排查时根本不知道哪张图卡在哪一步 - 别在重试逻辑里直接
go download(...)—— 协程池已负责调度,重复起 goroutine 就破防了
为什么用 io.Copy 而不是 io.ReadAll
io.ReadAll(resp.Body) 会把整张图读进内存再写文件,10MB 的图 × 10 并发 = 至少 100MB 内存常驻;而大图下载场景下,内存占用比 CPU 更早成为瓶颈。
立即学习“go语言免费学习笔记(深入)”;
- 直接
io.Copy(f, resp.Body),数据流式写入磁盘,内存峰值只取决于 OS page cache 大小 - 记得在
io.Copy前设好f.Sync()或用os.O_SYNC标志(视可靠性要求而定),否则断电可能丢最后几 KB - 如果要校验 MD5/SHA256,用
io.TeeReader(resp.Body, hashWriter),边读边算,不额外加载
事情说清了就结束。真正难的不是起十个协程,是让它们在出错、超时、路径异常、磁盘满、对方限速时,依然不泄漏、不卡死、不污染输出目录。










