【问题标题】:Quit all recursively spawned goroutines at once立即退出所有递归生成的 goroutine
【发布时间】:2025-12-05 02:20:15
【问题描述】:

我有一个函数可以递归地生成 goroutine 来遍历 DOM 树,将它们找到的节点放入所有节点共享的通道中。

import (
    "golang.org/x/net/html"
    "sync"
)

func walk(doc *html.Node, ch chan *html.Node) {
    var wg sync.WaitGroup
    defer close(ch)
    var f func(*html.Node)
    f = func(n *html.Node) {
        defer wg.Done()
        ch <- n
        for c := n.FirstChild; c != nil; c = c.NextSibling {
            wg.Add(1)
            go f(c)
        }
    }
    wg.Add(1)
    go f(doc)
    wg.Wait()
}

我会这样称呼

// get the webpage using http
// parse the html into doc
ch := make(chan *html.Node)
go walk(doc, ch)

for c := range ch {
    if someCondition(c) {
        // do something with c
        // quit all goroutines spawned by walk
    }
}

我想知道如何退出所有这些 goroutine——即关闭ch--一旦我找到某种类型的节点或满足其他一些条件。我尝试使用quit 通道,在生成新的 goroutine 之前会对其进行轮询,如果收到一个值,则关闭 ch,但这会导致一些 goroutine 尝试在刚刚被另一个关闭的通道上发送的竞争条件一。我正在考虑使用互斥锁,但使用互斥锁保护通道似乎不优雅并且违背了 go 的精神。有没有一种惯用的方式来使用频道来做到这一点?如果没有,有什么办法吗?任何意见表示赞赏!

【问题讨论】:

    标签: go


    【解决方案1】:

    context 包提供了类似的功能。使用 context.Context 和一些 Go-esque 模式,你可以实现你所需要的。

    首先,您可以通过contexthttps://www.sohamkamani.com/blog/golang/2018-06-17-golang-using-context-cancellation/查看这篇文章以更好地了解取消的感觉

    还请务必查看官方 GoDoc:https://golang.org/pkg/context/

    所以要实现这个功能,你的函数应该看起来更像:

    func walk(ctx context.Context, doc *html.Node, ch chan *html.Node) {
        var wg sync.WaitGroup
        defer close(ch)
    
        var f func(*html.Node)
        f = func(n *html.Node) {
            defer wg.Done()
    
            ch <- n
            for c := n.FirstChild; c != nil; c = c.NextSibling {
                select {
                case <-ctx.Done():
                    return // quit the function as it is cancelled
                default:
                    wg.Add(1)
                    go f(c)
                }
            }
        }
    
        select {
        case <-ctx.Done():
            return // perhaps it was cancelled so quickly
        default:
            wg.Add(1)
            go f(doc)
            wg.Wait()
        }
    }
    

    当调用函数时,你会得到类似的东西:

    // ...
    ctx, cancelFunc := context.WithCancel(context.Background())
    walk(ctx, doc, ch)
    for value := range ch {
        // ...
        if someCondition {
            cancelFunc()
            // the for loop will automatically exit as the channel is being closed for the inside
        }
    }
    

    【讨论】:

    • 这太好了,谢谢!我记得第一次进入 go 时浏览了 context 包并且不太了解它的用途,这很有帮助。