没有不值得去解决的问题,也没有不值得去学习的技术!

Go 语言并发实战:实现一个线程安全的 Web 爬虫

在 Go 语言的官方教程中,Web Crawler(网页爬虫)练习是一个非常经典的并发编程案例。它要求我们修改一个基础的递归爬虫,使其能够并行抓取 URL,同时确保同一个 URL 不会被重复抓取。

本文将带你一步步分析原始代码的问题,并利用 Go 的 sync 包实现一个高效、安全的并发爬虫。

一、 初始代码的问题

原始的 Crawl 函数虽然逻辑简单,但在实际应用中存在两个致命缺陷:

1. 串行执行:代码通过递归调用 Crawl 来处理子链接。这意味着程序必须等前一个页面及其所有子页面全部抓取完毕后,才会开始处理下一个兄弟节点。这完全浪费了现代多核 CPU 的性能,也没有体现 Go 语言“不要通过共享内存来通信,而要通过通信来共享内存”的并发哲学。

2. 重复抓取与死循环:如果网页 A 链接到 B,而 B 又链接回 A,原始的递归逻辑会陷入无限循环。即使没有循环,多个页面指向同一个资源时,也会导致重复的网络请求,浪费带宽和处理时间。

二、 核心解决方案

为了解决上述问题,我们需要引入两个关键机制:

1. 并发控制:使用 goroutine 并行发起网络请求。
2. 状态同步:使用互斥锁(Mutex)保护共享的“已访问”记录表,确保并发安全。

三、 代码实现详解

以下是完善后的完整代码实现:

Go
package main

import (
	"fmt"
	"sync"
)

type Fetcher interface {
	// Fetch returns the body of URL and
	// a slice of URLs found on that page.
	Fetch(url string) (body string, urls []string, err error)
}

// Crawl uses fetcher to recursively crawl
// pages starting with url, to a maximum of depth.
func Crawl(url string, depth int, fetcher Fetcher) {
	// visited 用于记录已经访问过的 URL,避免重复爬取
	visited := make(map[string]bool)
	var mu sync.Mutex     // 互斥锁,保护 visited 的并发访问
	var wg sync.WaitGroup // WaitGroup 用于等待所有爬取任务完成

	// crawl 是内部递归函数,实际执行爬取逻辑
	var crawl func(url string, depth int)
	crawl = func(url string, depth int) {
		defer wg.Done() // 确保每个 crawl 调用结束时调用 Done,通知 WaitGroup

		if depth <= 0 {
			return
		}

		mu.Lock() // 加锁,保护 visited 的访问
		if visited[url] {
			mu.Unlock() // 已访问过,解锁并返回
			return
		}
		visited[url] = true // 标记当前 URL 已访问
		mu.Unlock()         // 解锁

		body, urls, err := fetcher.Fetch(url)
		if err != nil {
			fmt.Println(err)
			return
		}
		fmt.Printf("found: %s %q\n", url, body)

		for _, u := range urls {
			wg.Add(1)            // 为每个新的爬取任务增加计数
			go crawl(u, depth-1) // 启动新的 goroutine 爬取子 URL,深度减 1
		}
	}

	wg.Add(1)            // 为初始 URL 增加计数
	go crawl(url, depth) // 启动爬取初始 URL 的 goroutine
	wg.Wait()            // 等待所有爬取任务完成
}

func main() {
	Crawl("https://golang.org/", 4, fetcher)
}

// fakeFetcher is Fetcher that returns canned results.
type fakeFetcher map[string]*fakeResult

type fakeResult struct {
	body string
	urls []string
}

func (f fakeFetcher) Fetch(url string) (string, []string, error) {
	if res, ok := f[url]; ok {
		return res.body, res.urls, nil
	}
	return "", nil, fmt.Errorf("not found: %s", url)
}

// fetcher is a populated fakeFetcher.
var fetcher = fakeFetcher{
	"https://golang.org/": &fakeResult{
		"The Go Programming Language",
		[]string{
			"https://golang.org/pkg/",
			"https://golang.org/cmd/",
		},
	},
	"https://golang.org/pkg/": &fakeResult{
		"Packages",
		[]string{
			"https://golang.org/",
			"https://golang.org/cmd/",
			"https://golang.org/pkg/fmt/",
			"https://golang.org/pkg/os/",
		},
	},
	"https://golang.org/pkg/fmt/": &fakeResult{
		"Package fmt",
		[]string{
			"https://golang.org/",
			"https://golang.org/pkg/",
		},
	},
	"https://golang.org/pkg/os/": &fakeResult{
		"Package os",
		[]string{
			"https://golang.org/",
			"https://golang.org/pkg/",
		},
	},
}

四、 关键技术点解析

1. 为什么需要 sync.Mutex?

在 Go 语言中,map 类型不是并发安全的。如果多个 goroutine 同时尝试读写同一个 map,程序会直接 panic 报错。因此,我们在检查 visited[url] 和设置 visited[url] = true 这两个操作之间,必须保证原子性。通过 mu.Lock() 和 mu.Unlock(),我们确保了同一时刻只有一个 goroutine 能修改或检查这个 map。

2. sync.WaitGroup 的作用

当我们启动多个 goroutine 时,主函数(main)不会自动等待它们结束。如果没有 WaitGroup,主函数可能在第一个请求发出后就立即退出,导致后续的子任务被强行终止。

wg.Add(1):在启动新的 goroutine 之前,将计数器加 1。
defer wg.Done():在 goroutine 执行结束时,将计数器减 1。
wg.Wait():阻塞主线程,直到计数器归零,即所有任务全部完成。

3. 闭包与递归的结合

我们在 Crawl 内部定义了一个局部函数 crawl。这样做的好处是它可以自由访问外部的 visited、mu 和 wg 变量,无需将这些参数层层传递。同时,通过 go crawl(u, depth-1),我们将递归转化为了并发的任务分发。

五、 总结

通过这个练习,我们不仅实现了一个简单的爬虫,更深刻理解了 Go 并发模型中的几个核心概念:

– 利用 goroutine 提升 I/O 密集型任务的效率。
– 使用 Mutex 保护共享状态,避免数据竞争(Data Race)。
– 使用 WaitGroup 协调多个协程的生命周期。

在实际的生产环境中,你可能还需要考虑增加超时控制、错误重试机制以及限制最大并发数(使用带缓冲的 channel 作为信号量),但这个基础版本已经涵盖了并发编程最核心的逻辑。

需要长期技术维护或远程问题排查?

我是拥有 15+ 年经验的 PHP / Go 后端工程师,长期关注已有系统维护、Bug 修复、性能优化、服务器排查、WordPress 网站维护和小功能迭代。

如果你的项目遇到以下情况,可以先从一次小问题排查开始合作:

  • ✅ PHP / Laravel / Yii2 老项目无人维护
  • ✅ Go / Gin 后端接口需要排查或优化
  • ✅ WordPress 网站访问慢、报错或插件冲突
  • ✅ Nginx / MySQL / Redis / Linux 服务器异常
  • ✅ CDN / Cloudflare / DNS / HTTPS 配置问题
  • ✅ 需要长期远程技术支持或兼职维护

更多介绍请查看:关于我 & 合作

微信:13980074657
邮箱:shuijingwanwq@gmail.com
Telegram:@shuijingwan
GitHub:https://github.com/shuijingwan

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理