裸写 go crawl(url) 必崩,因不控并发、不复用连接、无超时错误处理;应使用带缓冲 channel(如 sem := make(chan struct{}, 5))限流,并复用 http.client、配置 transport、为每个请求设独立 context 超时。

直接用 go crawl(url) 启一堆 goroutine 是最常见也最危险的做法——它不控制并发数、不复用 HTTP 客户端、不处理超时和错误,跑几分钟就卡死或被封。
为什么裸写 go crawl(url) 必崩
每个 go crawl(url) 都会启动一个新 goroutine,而每个 goroutine 内部调用 http.Get 时,默认会新建连接、重复 DNS 查询、不复用 TLS 会话。实际运行中:
• 几百个 URL 就触发 dial tcp: lookup failed(DNS 耗尽)
• 出现大量 context deadline exceeded(连接池满 + 默认超时太长)
• 进程打开文件数爆掉,系统报 socket: too many open files
用带缓冲 channel 做信号量限并发
这是最轻量、最可控的并发闸门,比 sync.WaitGroup + 计数器更直观,也比第三方 semaphore 库更少依赖。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
-
sem := make(chan struct{}, 5)—— 最多同时跑 5 个请求,数值 2~6 是安全起点 - 每发一个请求前先
sem ,阻塞直到有空位 - goroutine 结束时必须在内部
defer func() { ,否则信号量永远不释放 - 闭包捕获
url要用func(u string) { ... }(url),避免循环变量快照问题
HTTP 客户端必须全局复用并配置超时
每次 new http.Client 都会丢弃连接池、DNS 缓存和 TLS 会话,等于把网络层重置一遍。
- 定义全局变量:
var client = &http.Client{Timeout: 10 * time.Second} - 配 Transport:
client.Transport = &http.Transport{MaxIdleConnsPerHost: 100},否则默认只保持 2 个空闲连接 - 单次请求超时要用
context.WithTimeout,不能改 client.Timeout(非并发安全) - 务必
defer resp.Body.Close(),否则连接永不归还,MaxIdleConnsPerHost形同虚设
URL 去重用 map[string]struct{} 就够了
初学者总想上布隆过滤器,但真实场景里百万级 URL 占内存不到 100MB,且无需强一致性去重。
- 声明:
seen := make(map[string]bool),访问前查if !seen[url] - 成功抓取后立刻
seen[url] = true,别等解析完再写——防止重复提交任务 - 不用加锁:所有 worker 都从同一个 channel 拿任务,
seen只在主 goroutine 或加锁区域更新即可 - 如果爬取规模真到千万级,再考虑分片 + sync.Map,而不是一上来就引入复杂度
真正难的不是并发本身,而是让每个 goroutine 在出错、超时、重定向、压缩响应、连接中断这些真实网络条件下还能稳定交还资源——漏掉任意一环,跑半天后就会发现内存涨不动、goroutine 数卡在几千、日志里全是 net.ErrClosed 和 io.EOF。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










