一、替代方案

前言:我会使用一个更简单的生成器,因为问题不关心生成器的复杂性,而是关心生成器和消费者之间的信号,以及消费者本身的调用。这个简单的生成器只生成从0to的整数9。


1.带函数值

通过传递一个简单的消费者函数,生成消费者模式更加清晰,它还有一个优点,即如果需要中止或任何其他操作,它可以返回一个值信号。


由于在示例中只有一个事件要发出信号(“中止”),消费者函数将具有bool返回类型,在需要中止时发出信号。


所以看这个简单的例子,消费者函数值传递给生成器:


func generate(process func(x int) bool) {

    for i := 0; i < 10; i++ {

        if process(i) {

            break

        }

    }

}


func main() {

    process := func(x int) bool {

        fmt.Println("Processing", x)

        return x == 3 // Terminate if x == 3

    }

    generate(process)

}

输出(在Go Playground上试试):


Processing 0

Processing 1

Processing 2

Processing 3

请注意,使用者 ( process) 不需要是“本地”函数,它可以在 之外声明main(),例如,它可以是全局函数或来自另一个包的函数。


该解决方案的潜在缺点是它仅使用 1 个 goroutine 来生成和使用值。


2. 有渠道

如果你仍然想用频道来做,你可以。请注意,由于通道是由生成器创建的,并且由于消费者对从通道接收到的值进行循环(最好使用for ... range构造),因此生成器有责任关闭通道。解决此问题还允许您返回仅接收通道。


是的,关闭生成器中返回的通道最好作为延迟语句完成,因此即使生成器发生恐慌,消费者也不会被阻塞。但请注意,这个延迟关闭不是在generate()函数中,而是在匿名函数中generate(),作为一个新的 goroutine启动并执行;否则通道将在返回之前关闭generate()- 根本没有用......


如果您想从消费者处向生成器发出信号(例如中止而不生成更多值),您可以使用例如另一个通道,该通道传递给生成器。由于生成器只会“侦听”此通道,因此它也可以声明为生成器的仅接收通道。如果你只需要发出一个事件信号(在我们的例子中是中止),不需要在这个通道上发送任何值,一个简单的关闭就可以了。如果您需要向多个事件发送信号,可以通过在此通道上实际发送一个值来完成,即要执行的事件/操作(其中中止可能是多个事件中的一个)。


并且您可以使用该select语句作为处理在返回通道上发送值并观察传递给生成器的通道的惯用方式。


这是一个带有abort频道的解决方案:


func generate(abort <-chan struct{}) <-chan int {

    ch := make(chan int)

    go func() {

        defer close(ch)

        for i := 0; i < 10; i++ {

            select {

            case ch <- i:

                fmt.Println("Sent", i)

            case <-abort: // receive on closed channel can proceed immediately

                fmt.Println("Aborting")

                return

            }

        }

    }()

    return ch

}


func main() {

    abort := make(chan struct{})

    ch := generate(abort)

    for v := range ch {

        fmt.Println("Processing", v)

        if v == 3 { // Terminate if v == 3

            close(abort)

            break

        }

    }

    // Sleep to prevent termination so we see if other goroutine panics

    time.Sleep(time.Second)

}

输出(在Go Playground上试试):


Sent 0

Processing 0

Processing 1

Sent 1

Sent 2

Processing 2

Processing 3

Sent 3

Aborting

这个解决方案的明显优势是它已经使用了 2 个 goroutines(1 个生成值,1 个消耗/处理它们),并且很容易扩展它以使用任意数量的 goroutines 作为返回的通道来处理生成的值生成器可以同时从多个 goroutine 中使用 - 通道可以安全地同时接收,数据竞争不会发生,按照设计;更多阅读:如果我正确使用通道,我是否需要使用互斥锁?


二、未解决问题的答案

goroutine 上的“未捕获”恐慌将结束 goroutine 的执行,但不会导致资源泄漏问题。但是,如果作为单独的 goroutine 执行的函数会在非恐慌的情况下释放由它分配的资源(在非延迟语句中),那么该代码显然不会运行并且会导致例如资源泄漏。


您没有观察到这一点,因为程序在主协程终止时终止(并且它不会等待其他非主协程完成 - 因此您的其他协程没有机会恐慌)。请参阅规范:程序执行。


但是要知道,panic()并且recover()是针对例外情况,它们不适用于诸如try-catchJava 中的异常和块之类的一般用例。例如,应该通过返回错误(并处理它们!)来避免恐慌,并且恐慌绝对不应该离开包的“边界”(例如panic(),recover()可能有理由在包实现中使用,但恐慌状态应该被“捕获” " 放在包裹里面,不要从里面拿出来)。