You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 04:07:15