应使用带缓冲 channel 构建固定大小的 worker pool 控制并发数(5–20),配合 sync.WaitGroup 等待任务完成;每个请求需独立 context.WithTimeout 防止单点阻塞;结果通过 chan Result 收集,避免 map + mutex 竞争。

用 sync.WaitGroup 控制并发数量,别直接开几百个 goroutine
开太多 goroutine 不等于快,反而容易被目标网站限流、本地 fd 耗尽或内存暴涨。真正可控的并发必须显式限制 worker 数量。
常见错误是写成 for _, url := range urls { go fetch(url) } —— 这会瞬间启动全部 goroutine,完全失控。
- 用
semaphore(信号量)或带缓冲 channel 模拟固定大小的 worker pool,推荐后者:创建一个容量为 N 的chan struct{},每次取一个 token 再发请求,结束后放回 -
sync.WaitGroup仅用于等待所有任务完成,不能替代并发控制 - 建议并发数设为 5–20,取决于目标服务器承受力和本地资源;HTTP/2 支持复用连接,但 DNS 解析、TLS 握手仍是瓶颈
用 context.WithTimeout 统一管理超时,避免单个请求拖垮整体
不设超时的 http.Get 可能卡住几十秒,导致整个抓取队列阻塞。必须对每个请求单独设 timeout,且与整体流程解耦。
错误做法:只在最外层设一个 context,然后传给所有请求——这样某个慢请求会提前取消其他正常请求。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 每个
http.Request应使用独立的context.WithTimeout(ctx, 10*time.Second) - 主 context 用于控制整个任务生命周期(比如总耗时不超过 60 秒),子 context 用于单个请求
- 注意:
http.Client的Timeout字段是全局兜底,仍建议优先用 context,更灵活
状态收集用 chan Result 而不是 map + mutex,避免竞争和锁瓶颈
多个 goroutine 往同一个 map 写数据,必须加 sync.RWMutex,但锁争用严重,尤其结果量大时性能下降明显。
更自然的方式是让每个 worker 把结果发到一个共享 channel,主 goroutine 在 range 中接收并聚合——channel 天然线程安全,且能自然配合 WaitGroup 关闭。
- 定义
type Result struct { URL string; Status int; Body []byte; Err error } - worker 结束后
resultChan ,主 goroutine 启一个 goroutine 关闭 channel:<code>go func() { wg.Wait(); close(resultChan) }() - 切忌在 worker 里直接往 slice append——仍需锁,且无法控制顺序
实际调用时记得设置 http.Client 的 Transport,否则默认连接池太激进
Go 默认的 http.DefaultClient 使用 http.Transport,其 MaxIdleConns 和 MaxIdleConnsPerHost 默认都是 100,高并发下容易占满连接,触发 “too many open files” 错误。
- 显式构造 client:
&http.Client{Transport: &http.Transport{MaxIdleConns: 20, MaxIdleConnsPerHost: 20}} - 如果目标域名集中(如全是 example.com),更要调低
MaxIdleConnsPerHost,避免单 host 占满连接 - 可选:加
IdleConnTimeout: 30 * time.Second防止空闲连接堆积
并发抓取不是堆 goroutine 数量,而是平衡请求节奏、连接复用和错误隔离。最容易被忽略的是 Transport 配置和单请求 context —— 它们不出错时不显眼,一出错就整批失败。

















