大致介绍
Crawler 并不是该课程的 Lab,只是第二节课里讲述并发 IO 和互斥锁的一个例子。
但是这个例子很重要,它涉及并发编程的核心思想,即锁和线程通信。
这个例子里使用里共享内存和管道通信两种方式来实现网页爬取任务。
具体代码
crawler.go
该方法采取共享内存,对 Map 使用锁来避免死锁
go
package main
import (
"fmt"
"sync"
)
// 网页获取器接口
type Fetcher interface {
Fetch(url string) (body string, urls []string, err error)
}
// 已访问 URL 集合(带互斥锁保护)
var visited = struct {
sync.Mutex
m map[string]bool
}{m: make(map[string]bool)}
// 递归爬取网页
func Crawl(url string, depth int, fetcher Fetcher, wg *sync.WaitGroup) {
defer wg.Done()
if depth <= 0 {
return
}
// 检查是否已访问,去重
visited.Lock()
if visited.m[url] {
visited.Unlock()
return
}
visited.m[url] = true
visited.Unlock()
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("found %s %q\n", url, body)
// 并发爬取子链接
for _, url := range urls {
wg.Add(1)
go Crawl(url, depth+1, fetcher, wg)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(1)
go Crawl("https://golang.org/", 3, fetcher, &wg)
wg.Wait()
}
// 模拟获取器
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)
}
// 模拟数据
var fetcher = fakeFetcher{
"https://golang.org/": {
"The Go Programming Language",
[]string{
"https://golang.org/pkg/",
"https://golang.org/cmd/",
},
},
"https://golang.org/pkg/": {
"Packages",
[]string{
"https://golang.org/",
"https://golang.org/cmd/",
"https://golang.org/pkg/fmt/",
"https://golang.org/pkg/os/",
},
},
"https://golang.org/pkg/fmt/": {
"Package fmt",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
"https://golang.org/pkg/os/": {
"Package os",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
}crawler_chan.go
该方法使用 channel 通信来共享内存
go
package main
import (
"fmt"
"sync"
)
// 爬取任务
type fetchTask struct {
url string
depth int
}
// 使用 Channel 实现的并发爬虫(去掉了显式锁)
func CrawlChan(url string, depth int, fetcher Fetcher) {
tasks := make(chan fetchTask)
done := make(chan struct{})
// 统一管理 visited 状态,无需加锁
go func() {
visited := make(map[string]bool)
var wg sync.WaitGroup
// 提交任务
submit := func(u string, d int) {
if d <= 0 || visited[u] {
return
}
visited[u] = true
wg.Add(1)
go func() {
defer wg.Done()
body, urls, err := fetcher.Fetch(u)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("found %s %q\n", u, body)
for _, next := range urls {
tasks <- fetchTask{next, d - 1}
}
}()
}
// 从 channel 读取新任务
go func() {
for t := range tasks {
submit(t.url, t.depth)
}
}()
submit(url, depth)
wg.Wait()
close(tasks)
close(done)
}()
<-done
}crawler_test.go
测试文件
go
package main
import (
"sync"
"testing"
)
// 测试 Crawl 是否正常爬取
func TestCrawl(t *testing.T) {
var wg sync.WaitGroup
wg.Add(1)
go Crawl("https://golang.org/", 3, fetcher, &wg)
wg.Wait()
visited.Lock()
_, ok := visited.m["https://golang.org/"]
visited.Unlock()
if !ok {
t.Errorf("expected https://golang.org/ to be visited")
}
}测试结果
![[Pasted image 20260621234550.png]]