goroutine 本身不决定遍历策略,深度/广度由任务分发逻辑和url入队顺序控制;需在task中携带depth字段并由parser显式限制递进,配合channel与worker pool实现可控并发爬取。

goroutine 如何支撑深度优先 / 广度优先爬取逻辑
goroutine 本身不决定遍历策略,它只是执行单元;真正控制“深度”的是任务分发逻辑和 URL 入队顺序。你用 go fetch(url) 启动一个协程,不代表它会自动递归抓子链接——必须显式解析 HTML、提取新 URL、再把它们推入任务通道。
常见错误是把所有链接一股脑塞进同一个 chan string,导致深度失控(比如首页抓到 100 个链接,每个又带出 100 个,瞬间爆炸)。正确做法是按层级或深度标记分发:
- 在请求结构体中携带
depth字段,例如type Request struct { URL string; Depth uint } - worker 解析响应后,只将
nextDepth := req.Depth + 1≤maxDepth的新 URL 推入任务通道 - 避免用递归函数调用
go fetch(),否则栈深度不可控,且无法统一限深
为什么直接用 goroutine 递归会导致崩溃
有人写类似这样的代码:func crawl(url string, depth int) { ... go crawl(nextURL, depth+1) ... }。这看着像深度优先,实则危险:
- 没有并发数限制:每层都 spawn 新 goroutine,几层下来就上万 goroutine,内存爆满
- 无超时/重试控制:某个子链接卡住,整个调用链挂死
- 无法统计进度:没有中心化任务队列,
sync.WaitGroup很难准确 Add/Done - URL 去重失效:不同 goroutine 同时发现同一 URL,重复抓取
本质问题在于混淆了「执行并发」和「调度逻辑」——goroutine 负责跑,但谁给它任务、给多少、给什么,得靠 channel + worker pool 控制。
如何用 channel + depth 控制真实深度抓取
核心是让任务生成者(parser)和执行者(worker)解耦,且 depth 成为任务元数据的一部分。示例关键片段:
type Task struct {
URL string
Depth uint
}
<p>tasks := make(chan Task, 1000)
results := make(chan Result, 100)</p><p>// 启动固定数量 worker
for i := 0; i </p><p>// 主协程:初始 URL 入队
tasks https://www.php.cn/link/b05edd78c294dcf6d960190bf5bde635", Depth: 0}</p><p>// parser 在收到响应后,只推送合法 depth 的子链接
func parseHTML(body []byte, task Task) {
if task.Depth >= maxDepth {
return
}
for _, link := range extractLinks(body) {
tasks </p><p>注意:<code>tasks</code> 缓冲区大小、worker 数量、<code>maxDepth</code> 值三者共同决定实际抓取广度与深度平衡点。调大 buffer 不等于能抓更深——它只缓解生产者阻塞,depth 判断仍在 parser 里做。</p><h3>容易被忽略的深度相关坑点</h3><p>深度抓取不是加个 <code>Depth</code> 字段就万事大吉。真实场景中这几个点常被跳过:</p>
- 相对 URL 拼接错误:抓到
/about却没拼对 base domain,导致Depth=2的请求发向错误站点 - 重定向未跟踪 depth:302 跳转后新 URL 应继承原
Depth,而非重置为 0 - robots.txt 或 meta robots 没校验:即使
Depth=3,目标页声明noindex,也该提前丢弃 - 循环引用未检测:A→B→C→A,depth 递增但实际进入环,需用
urlMap(如map[string]bool)全局去重,而非仅靠 depth 截断
depth 是软约束,URL 去重和域名白名单才是硬边界。别指望靠 depth 数字挡住无限递归——它只负责剪枝,不负责兜底。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











