
本文详解go tour中web crawler习题的简洁可靠解法,聚焦如何用sync.waitgroup替代错误的channel关闭逻辑,避免死锁,确保所有goroutine完成后再安全退出;同时优化并发安全缓存设计。
本文详解go tour中web crawler习题的简洁可靠解法,聚焦如何用sync.waitgroup替代错误的channel关闭逻辑,避免死锁,确保所有goroutine完成后再安全退出;同时优化并发安全缓存设计。
在Go Tour的“Web Crawler”练习中,初学者常陷入一个典型陷阱:试图通过监听未关闭的无缓冲channel(如ress和errs)并依赖ok标志判断“任务结束”,结果导致主goroutine无限阻塞、子goroutine泄漏,最终触发deadlock。根本原因在于:channel的关闭必须由发送方显式完成,而goroutine的生命周期无法通过channel长度或读取状态准确推断。
正确的解法不是“何时关闭channel”,而是“如何精确跟踪所有并发任务是否完成”。sync.WaitGroup正是为此而生的标准工具——它不依赖共享状态或通道通信,而是以原子方式计数活跃goroutine,提供清晰、无竞态、零死锁的同步语义。
以下是一个精简、健壮、生产级可参考的实现:
package main
import (
"fmt"
"sync"
)
type Fetcher interface {
Fetch(url string) (body string, urls []string, err error)
}
// SafeCache 使用 RWMutex 提升读多写少场景性能(比纯Mutex更高效)
type SafeCache struct {
cache map[string]bool
mux sync.RWMutex // 读操作用RLock,写操作用Lock
}
func (c *SafeCache) Set(url string) {
c.mux.Lock()
c.cache[url] = true
c.mux.Unlock()
}
func (c *SafeCache) Get(url string) bool {
c.mux.RLock()
defer c.mux.RUnlock()
return c.cache[url]
}
var sc = SafeCache{cache: make(map[string]bool)}
// Crawl 是并发安全的递归爬取函数
func Crawl(url string, depth int, fetcher Fetcher, wg *sync.WaitGroup) {
defer wg.Done() // ✅ 每个goroutine结束时必调用Done()
if depth <= 0 {
return
}
// 去重检查(读操作)
if sc.Get(url) {
return
}
// 标记已访问(写操作)
sc.Set(url)
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Printf("Error fetching %s: %v\n", url, err)
return
}
fmt.Printf("found: %s %q\n", url, body)
// 启动子任务(深度优先转为广度/并发优先)
wg.Add(len(urls)) // ✅ 预先声明将启动的goroutine数量
for _, u := range urls {
go Crawl(u, depth-1, fetcher, wg)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(1) // ✅ 主任务计入等待
go Crawl("http://golang.org/", 4, fetcher, &wg)
// 等待所有goroutine完成(包括嵌套启动的所有子任务)
wg.Wait() // ✅ 安全、阻塞、无死锁
fmt.Println("Crawling completed.")
}✅ 关键改进说明:
立即学习“go语言免费学习笔记(深入)”;
-
WaitGroup驱动生命周期:
wg.Add()在goroutine启动前调用,defer wg.Done()在函数末尾保证执行,wg.Wait()在main中同步等待全部完成——三者构成闭环,彻底消除对channel关闭时机的猜测。 -
RWMutex优化缓存:因
Get()远多于Set(),使用RWMutex允许多个goroutine并发读取,仅写入时加独占锁,显著提升高并发吞吐。 -
无channel阻塞风险:移除了不可靠的
for { select { ... } }循环,避免因channel未关闭导致的永久挂起。 -
错误处理内聚:错误直接打印而非发往channel,简化流程;若需集中错误收集,可配合带缓冲的error channel +
wg协同关闭(但本题无需)。
⚠️ 注意事项:
- 不要将
wg作为全局变量传递,应显式传参,避免隐式依赖和测试困难; -
wg.Add()必须在go语句之前调用,否则存在竞态(goroutine可能在Add前就执行Done); -
wg.Wait()只能被调用一次,且不能在goroutine中调用(除非你明确需要非阻塞等待); - 若需支持取消(如超时),应结合
context.Context,但本练习聚焦基础并发模型,WaitGroup已足够。
这个方案直击Go并发本质:用组合原语(goroutine + WaitGroup + RWMutex)替代脆弱的手动channel协调。它代码量少、逻辑清晰、符合Go惯用法,是理解“并发即通信”之外另一重要范式——“并发即协作”的绝佳范例。


















