Go并发爬虫关键在于控并发、防崩、防封;需用带缓冲channel实现信号量限流,归一化URL并用sync.Map去重,限制响应体大小并确保resp.Body.Close()。

Go 语言实现并发爬虫的关键不在“能不能并发”,而在于“怎么控并发、怎么防崩、怎么不被封”。盲目开成百上千个 goroutine 发 http.Get,大概率触发连接耗尽、DNS 超时、服务端限流或本地文件描述符不足(too many open files)。
用 semaphore 控制并发请求数量
别靠 time.Sleep 或空 for 循环压节奏。标准做法是用带缓冲的 channel 模拟信号量,限制同时活跃的 HTTP 请求数量。
常见错误:直接对每个 URL 启一个 go fetch(url),没节制 —— 1000 个 URL 就起 1000 个 goroutine,底层 TCP 连接、DNS 查询、TLS 握手全堆在一起,系统先扛不住。
- 设一个
sem := make(chan struct{}, 10),表示最多 10 个并发请求 - 每次发请求前写入:
sem - 请求结束(无论成功失败)后必须释放:
- 这个 channel 不要 close,也不用 defer —— 它是长期复用的资源
http.Client 必须复用并配置超时
每个 http.Client 实例自带连接池;反复 new http.Client 会导致连接泄漏、TIME_WAIT 爆满、DNS 缓存失效。
立即学习“go语言免费学习笔记(深入)”;
默认的 http.DefaultClient 虽可用,但超时为 0(无限等待),极易卡死整个 goroutine。
- 定义全局或包级变量:
var client = &http.Client{Timeout: 10 * time.Second} - 设置
Transport复用连接:client.Transport = &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 100, IdleConnTimeout: 30 * time.Second, } - 不要在 handler 或循环里反复 new
http.Client,哪怕只改 Timeout
解析 HTML 前先检查 resp.StatusCode 和 Content-Type
很多爬虫一拿到 *http.Response 就直接丢给 golang.org/x/net/html 解析,结果遇到 404 页面、JSON 接口、重定向响应、二进制文件(PDF/图片),轻则 panic(invalid character),重则静默跳过关键错误。
- 务必检查:
if resp.StatusCode = 300 - 检查 Content-Type 是否含
text/html或application/xhtml+xml,否则跳过解析 - 用
io.LimitReader(resp.Body, 1024*1024)防止下载超大响应体(如视频页面嵌了 100MB 日志文件) - 记得
resp.Body.Close()—— 不关会泄漏连接,尤其在复用 client 时
URL 去重与避免重复抓取要用 sync.Map + 归一化
原始 URL 可能带不同 query 参数(?utm_source=xx)、大小写路径、末尾斜杠差异,直接字符串比较会导致重复抓取或漏抓。
并发环境下用普通 map[string]bool 会 panic,必须线程安全。
- 用
var visited = sync.Map{}存已抓 URL(key 是归一化后的字符串) - 归一化至少做三件事:转小写、移除 fragment(# 后内容)、标准化 query(按 key 排序再拼)
- 判断是否已访问:
if _, ok := visited.Load(normalizedURL); ok { continue } - 成功解析后存入:
visited.Store(normalizedURL, struct{}{})
真正难的不是并发本身,而是当 50 个 goroutine 同时在解析、去重、写磁盘、重试 429 响应时,哪条路径没加锁、哪个 error 被忽略、哪个 body 忘了 close —— 这些细节才决定爬虫跑一天后是稳如磐石,还是凌晨三点开始疯狂报 dial tcp: lookup xxx: no such host。










