使用sync.WaitGroup实现Go Web爬虫出现死锁panic,正确惯用法是什么?
问题根因
- 互斥锁未正常释放:你在
Crawl函数中调用mutex.Lock()加锁后,当判断URL已经被爬取过的分支直接返回时,没有执行mutex.Unlock()释放锁,导致锁被永久占用,其他所有操作urlCache的goroutine都会陷入无限等待,最终触发所有goroutine休眠的死锁错误。 - main函数逻辑顺序错误:你在执行
wg.Wait()等待所有爬取协程结束之前,就打印了Main: Completed,属于逻辑顺序错误,虽不直接导致死锁,但不符合预期执行流程。
修复方案
核心问题修复
- 补全锁释放逻辑:可以直接在已爬取判断分支中新增解锁操作,更推荐的方式是加锁后立即用
defer声明解锁,避免所有分支遗漏释放:
mutex.Lock() defer mutex.Unlock() // 加锁后立即声明延迟解锁,所有返回分支都会自动执行解锁 if alreadyFetched := urlCache[url]; alreadyFetched { fmt.Printf("Crawl: %v already fetched\n", url) return } urlCache[url] = true
- 调整main函数执行顺序,先等待协程执行完成再打印完成日志:
func main() { var wg sync.WaitGroup fmt.Println("Main: Starting worker") wg.Add(1) go Crawl("https://golang.org/", 4, fetcher, &wg) fmt.Println("Main: Waiting for workers to finish") wg.Wait() fmt.Println("Main: Completed") }
符合Go开发习惯的优化建议
- 避免使用全局存储:可以将
urlCache和mutex封装到自定义的爬取器结构体中,作为参数传递给Crawl函数,降低代码耦合,同时支持多个爬取任务并行执行互不干扰。 - 可以进一步用channel代替
sync.WaitGroup实现协程调度,更符合Go「通过通信共享内存」的设计哲学,不过针对当前练习场景,修复锁问题后使用WaitGroup的方案已经足够简洁可用。
内容的提问来源于stack exchange,提问作者Evgeni Petrov
相关产品推荐
相关产品推荐

