直接用 goroutine + http.Client 易被封或超时,因连接复用缺失、请求头雷同、无延迟控制、DNS 未缓存,且默认 Transport 连接数限制严;应自定义 Transport、启用 DNS 缓存、设合理 Header 与限速。

为什么直接用 goroutine + http.Client 会很快被封或超时
Go 的并发模型看似适合爬虫,但裸写 go fetch(url) 很快触发目标站的反爬机制:连接复用缺失、请求头雷同、无延迟控制、DNS 查询未复用。更关键的是,http.DefaultClient 的 Transport 默认只允许最多 100 个空闲连接,且不复用 DNS 缓存,高频请求下大量新建 TCP 连接,容易被识别为扫描行为。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 显式配置
http.Transport,设置MaxIdleConns(如 200)、MaxIdleConnsPerHost(如 100)、IdleConnTimeout(如 30 * time.Second) - 启用 DNS 缓存:用
net.Resolver配合transport.DialContext实现自定义 DNS 解析,避免每次请求都查 DNS - 所有请求必须带合理
User-Agent、Accept等 header,且可轮换(例如从切片中随机取) - 对同一域名加请求间隔(如
time.Sleep(100 * time.Millisecond)),用rate.Limiter更稳妥
如何安全地控制并发数与任务分发
用 sync.WaitGroup + 无缓冲 channel 直接塞 URL 容易导致 goroutine 泛滥或 channel 阻塞;而用 for i := 0; i 又难统一收口。正确做法是「固定 worker 数量 + channel 控制输入流」。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 启动固定数量(如 5–20)的 worker goroutine,每个从一个
chan string中读取 URL - 用
sync.WaitGroup管理 worker 生命周期,worker 内部用defer wg.Done() - 关闭 channel 前确保所有 URL 已发送完毕,否则部分 worker 会提前退出;可用
close(urls)触发所有 worker 自然退出 - 别把解析逻辑(如 XPath、CSS 选择器)也塞进 worker——提取出单独的
parse(resp *http.Response) []string函数,便于测试和复用
遇到重定向、429、503 怎么自动降频与重试
默认 http.Client 会自动跟随 3xx 重定向,但有些反爬中间件(如 Cloudflare)返回 429 或 503 后还附带 Retry-After 头,盲目重试只会加重封禁风险。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 禁用自动重定向:
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse } - 手动检查响应状态码:对 429 / 503,读取
resp.Header.Get("Retry-After"),转成秒数后 sleep 对应时间;若无该 header,则用指数退避(如第一次 1s、第二次 2s、第三次 4s) - 重试次数限制在 3 次以内,超过则记录日志并丢弃任务
- 把重试逻辑封装进独立函数,例如
fetchWithRetry(url string, client *http.Client, limiter *rate.Limiter) (*http.Response, error),避免每个 worker 重复写
如何避免内存泄漏与 goroutine 泄露
常见错误是:channel 未关闭、HTTP body 未 Close()、worker 在 panic 后未 recover、或用 time.After 导致 timer 不释放。这些都会让 goroutine 持续挂起,最终 OOM。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 每个
http.Response.Body必须在使用后调用resp.Body.Close(),哪怕只读了 status code —— 否则连接无法复用 - worker 内部用
defer func() { if r := recover(); r != nil { log.Printf("panic: %v", r) } }()拦截 panic,防止 goroutine 消失无踪 - 不要用
time.After做单次超时,改用context.WithTimeout,确保超时后整个请求链能及时释放资源 - 用
pprof定期检查 goroutine 数量:curl http://localhost:6060/debug/pprof/goroutine?debug=1,上线前务必验证峰值是否稳定
真正难的不是并发数量,而是让每个 goroutine 都“守规矩”:按时关连接、按需睡时间、按规则退避、按路径清理资源。漏掉其中一环,跑两天就卡死或被限速,比单线程还慢。










