
在 go 并发爬虫中,不能依赖 channel 长度或盲目关闭 channel 来判断任务结束;应使用 `sync.waitgroup` 精确跟踪活跃 goroutine 数量,确保所有爬取任务完成后再退出。
在实现类似 Go Tour 并发练习:Web Crawler 的任务时,一个常见误区是试图通过检查 channel 缓冲区长度(如 len(stor.Queue) == 0)来判断“是否还有任务”,甚至提前关闭 channel——这不仅逻辑错误(channel 关闭后无法再发送,但新 URL 可能仍在生成),更会导致死锁或 panic。
根本问题在于:channel 本身不表达“任务完成”的语义;它只是数据传递的管道。真正需要回答的是:“所有已启动的 goroutine 是否都已执行完毕?”——这正是 sync.WaitGroup 的设计目标。
✅ 正确做法:用 WaitGroup 管理生命周期
WaitGroup 提供三个核心方法:
- Add(n):增加待等待的 goroutine 计数;
- Done():标记一个 goroutine 完成(需在 defer 中调用,确保异常退出也能计数);
- Wait():阻塞直到计数归零。
在爬虫中,我们只需:
- 每次启动新 goroutine 前调用 wg.Add(1);
- 在 Crawl 函数末尾 defer wg.Done();
- 主函数中 wg.Wait() 等待全部完成。
以下是精简、线程安全的完整实现(移除了易出错的 channel 队列,改用纯 goroutine 分发):
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var visited = make(map[string]int) // 全局共享,注意:实际生产环境需加 mutex,本例因无并发写冲突可暂省略
type Result struct {
Url string
Depth int
}
type Fetcher interface {
Fetch(url string) (body string, urls []string, err error)
}
func Crawl(res Result, fetcher Fetcher) {
defer wg.Done() // 确保无论成功/失败都计数减一
if res.Depth <= 0 {
return
}
url := res.Url
if visited[url] > 0 { // 已访问过,跳过
fmt.Println("skip:", url)
return
}
visited[url] = 1
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println("fetch error:", url, err)
return
}
fmt.Printf("found: %s %q\n", url, body)
// 并发处理子链接
for _, u := range urls {
wg.Add(1) // 关键:为每个新 goroutine 预先计数
go Crawl(Result{u, res.Depth - 1}, fetcher)
}
}
func main() {
wg.Add(1) // 启动初始爬取任务
Crawl(Result{"http://golang.org/", 4}, fetcher)
wg.Wait() // 阻塞等待所有 goroutine 结束
fmt.Println("Crawling finished.")
}⚠️ 注意事项与进阶建议
- 竞态风险:本例中 visited 是全局 map,多个 goroutine 同时写入存在数据竞争。真实项目中必须加 sync.Mutex 或改用 sync.Map(适用于读多写少场景)。
- 避免 channel 误用:原代码中 stor.Queue 本质是模拟任务队列,但未配合同步机制(如 close() 时机难控、消费者无法感知“最后一条”),反而增加复杂度。纯 goroutine 分发 + WaitGroup 更简洁可靠。
- 深度控制与终止条件:Depth 是天然的递归终止条件,配合 visited 去重,即可保证有限图上的收敛。
- 扩展性提示:若需限速、超时、错误重试或结果收集,可在 Crawl 中引入 context.Context 和带缓冲的 result channel,但 WaitGroup 仍是基础同步原语。
总之,判断“何时不再有数据”在并发爬虫中,不是问 channel 还有没有值,而是问“所有工作单元是否已退出”。sync.WaitGroup 是 Go 标准库为此场景提供的最直接、最可靠的工具。









