Golang递归爬虫出现阻塞状态的原因及解决方法
Go爬虫Goroutine阻塞问题分析与解决
问题原因
- 无缓冲Channel引发发送阻塞:当前
jobs是无缓冲Channel(make(chan job)),当Worker正在处理任务时,enqueue中的c.jobs <- j会因无空闲接收方而阻塞。比如workers=1时,唯一的Worker卡在runJob执行流程中,无法同时读取Channel,新任务发送操作会卡住,导致runJob无法继续调用wg.Done(),最终wg.Wait()永久等待,形成死锁。 - WaitGroup与Channel关闭顺序错误:
Start函数中先执行c.wg.Wait()再关闭jobs,但如果enqueue因Channel阻塞无法完成,WaitGroup计数永远无法归零,Wait()会一直阻塞,后续的close(c.jobs)永远执行不到。 - HTTP超时设置无效:
http.Get未绑定创建的pageCtx,导致超时配置不生效,若请求长时间无响应会持续占用Worker资源。 - 错误处理隐患:当
http.Get失败时,resp可能为nil,此时调用resp.Body.Close()会触发panic。
解决方案
1. 将Jobs Channel改为带缓冲
创建Channel时设置合适的缓冲大小,避免发送任务时立即阻塞,缓冲大小可根据Worker数量调整(比如Worker数的2倍):
func NewCrawler(db db.DB, workers int, timeout int, imagesFolderName string) *Crawler { return &Crawler{ // 按Worker数设置缓冲,可根据实际业务调整 jobs: make(chan job, workers*2), workers: workers, db: db, timeout: timeout, imagesFolderName: imagesFolderName, visited: &sync.Map{}, wg: &sync.WaitGroup{}, } }
2. 调整WaitGroup与Channel关闭逻辑
将wg.Wait()放在单独Goroutine中执行,确保所有任务完成后再关闭Channel,避免阻塞主流程:
func (c *Crawler) Start(startingLinks []string) { if err := os.MkdirAll("apps/imagecrawler/"+c.imagesFolderName, os.ModePerm); err != nil { fmt.Printf("Error creating image directory: %v\n", err) os.Exit(1) } for i := 0; i < c.workers; i++ { go func() { for j := range c.jobs { c.runJob(j) c.wg.Done() } }() } for _, link := range startingLinks { c.enqueue(job{URL: link, Depth: 0}) } // 单独Goroutine等待所有任务完成后关闭Channel go func() { c.wg.Wait() close(c.jobs) }() }
3. 优化任务入队逻辑,提前过滤重复链接
在enqueue阶段判断链接是否已访问,避免重复入队和无效的WaitGroup计数:
func (c *Crawler) enqueue(j job) { _, alreadyVisited := c.visited.LoadOrStore(j.URL, true) if alreadyVisited { return } c.wg.Add(1) c.jobs <- j }
同时移除runJob中的重复判断逻辑,优化后代码:
func (c *Crawler) runJob(j job) { pageCtx, pageCancel := context.WithTimeout(context.Background(), time.Minute*time.Duration(c.timeout)) defer pageCancel() // 用defer确保Context被取消,避免资源泄漏 fmt.Println("Crawling URL", j.URL, "Depth:", j.Depth) // 使用Context创建HTTP请求,确保超时生效 req, err := http.NewRequestWithContext(pageCtx, "GET", j.URL, nil) if err != nil { fmt.Println("Create request error:", err) return } resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println("Fetching error:", err) return } defer resp.Body.Close() // 用defer确保响应体被关闭 b, err := io.ReadAll(resp.Body) if err != nil { fmt.Println("Reading body: ", err) return } if j.Depth >= maxDepth { return } links, err := extracter.ExtractLinks(pageCtx, b, j.URL) if err != nil { fmt.Println("ExtractLinks Err", err) return } for _, link := range links { c.enqueue(job{URL: link, Depth: j.Depth + 1}) } }
4. 修复资源泄漏与错误处理
- 使用
defer自动关闭HTTP响应体和取消Context,避免资源泄漏。 - 用
http.NewRequestWithContext替代http.Get,让超时配置生效。 - 移除
http.Get失败时的resp.Body.Close()调用,避免nil指针panic。
内容的提问来源于stack exchange,提问作者Nick
相关产品推荐
相关产品推荐

