并发数应根据http连接池和目标站限速实际设定,而非凭感觉;默认maxidleconnsperhost=2会导致高并发虚高、耗时翻倍、超时暴增;需实测起步5–10,并显式配置transport参数;大文件宜分片,小文件才适用高并发;worker内必须在for循环中recover panic,否则任务静默丢失。

并发数设多少才不拖垮服务
别凭感觉设 100 或 1000,真实瓶颈往往卡在 HTTP 连接池或目标站限速上。默认 http.DefaultTransport.MaxIdleConnsPerHost 是 2,哪怕你开了 50 个 goroutine,最多只有 2 个能真正发请求,其余全在排队等连接——结果是并发数虚高、整体耗时翻倍、超时错误暴增。
- 先查目标服务的反爬策略:短时间大量请求可能被限速或封 IP,实测建议从 5–10 起步,逐步加压
- 显式配置
http.Transport,把MaxIdleConnsPerHost设为 >= 你期望的并发数(比如设 20 就配 20),同时设IdleConnTimeout防堆积 - 文件越大,并发收益越低:单个大文件用分片下载更有效;大批小文件才适合高并发 worker 池
worker 退出时 panic 会悄悄吃掉任务
一个未 recover 的 panic 让 worker goroutine 直接退出,channel 里后续任务没人消费,整个池子“ silently shrink”——并发数越来越少,但你根本察觉不到。这不是偶发问题,而是常见漏点。
- recover 必须放在 worker 的 for 循环内部,不是外层函数里:
for { select { case url := 这个循环体里包一层 <code>defer func() { if r := recover(); r != nil { log.Printf("worker panic: %v", r) } }() - 别依赖全局
http.Client:它没设Timeout或Transport,单个卡死请求会让整个 worker 挂住;每个请求用context.WithTimeout(ctx, 30*time.Second)包裹 - worker 退出前必须
wg.Done(),否则wg.Wait()永远不返回,进程无法优雅退出
channel 缓冲不是并发控制,别搞混了
make(chan string, 100) 只是缓冲区,不是并发上限。真正限制并发的是 worker 数量,缓冲区只是防止生产者(比如 filepath.Walk)被阻塞。缓冲区过大反而掩盖真实瓶颈——任务全堆在 channel 里,worker 却因 I/O 卡住不动,内存悄悄涨到 OOM。
- 任务 channel 推荐用小缓冲,比如
make(chan string, 16);无缓冲也行,但生产者侧必须配合非阻塞发送(select { case ch ) - 别在
filepath.Walk回调里直接ch :如果 channel 满了且无缓冲,整个遍历就卡死。稳妥做法是先收集路径切片,再批量发;或用 <code>sync.WaitGroup等 Walk 完再 close channel - 输出结果 channel(如
chan DownloadResult)建议带缓冲,容量设为输入总数或略大,避免 worker 写结果时阻塞
写大文件时 io.Copy 不是万能解药
对 >500MB 文件直接 io.Copy(dst, resp.Body),底层频繁分配小内存块,GC 压力陡增,P99 延迟毛刺明显,甚至触发 runtime: out of memory。
- 用
bufio.NewWriterSize(f, 1(1MB 缓冲)包装文件写入器,减少系统调用和内存碎片 - 更稳的做法是
io.CopyBuffer(dst, src, make([]byte, 1,复用固定缓冲区,避免 runtime 频繁 malloc - 下载前先
os.Truncate或预分配空间(f.Seek(size-1, io.SeekStart); f.Write([]byte{0})),防止磁盘满导致写入中途失败
真正的难点不在启动多少 goroutine,而在每个 worker 是否能在任意异常(网络断、panic、超时)下不泄漏、不丢任务、不卡死。这些边界处理没做全,池子建得再漂亮,跑两天就 quietly degrade。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











