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
http.Client 必须复用并配置超时每个 http.Client 实例自带连接池;反复 new http.Client 会导致连接泄漏、TIME_WAIT 爆满、DNS 缓存失效。
默认的 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,
}http.Client,哪怕只改 Timeoutresp.StatusCode 和 Content-Type
很多爬虫一拿到 *http.Response 就直接丢给 golang.org/x/net/html 解析,结果遇到 404 页面、JSON 接口、重定向响应、二进制文件(PDF/图片),轻则 panic(invalid character),重则静默跳过关键错误。
if resp.StatusCode = 300
text/html 或 application/xhtml+xml,否则跳过解析io.LimitReader(resp.Body, 1024*1024) 防止下载超大响应体(如视频页面嵌了 100MB 日志文件)resp.Body.Close() —— 不关会泄漏连接,尤其在复用 client 时sync.Map + 归一化原始 URL 可能带不同 query 参数(?utm_source=xx)、大小写路径、末尾斜杠差异,直接字符串比较会导致重复抓取或漏抓。
并发环境下用普通 map[st 会 panic,必须线程安全。
var visited = sync.Map{} 存已抓 URL(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。