recursion – 在Golang中为递归函数实现生成器(yield)的惯用方法

前端之家收集整理的这篇文章主要介绍了recursion – 在Golang中为递归函数实现生成器(yield)的惯用方法前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。
[注意:我读了 Python-style generators in Go,这不是它的重复. ]

在Python / Ruby / JavaScript / ECMAScript 6中,可以使用语言提供的yield关键字编写生成函数.在Go中,可以使用goroutine和channel模拟它.

代码

以下代码显示了如何实现置换函数(abcd,abdc,acbd,acdb,…,dcba):

// $src/lib/lib.go

package lib

// private,starts with lowercase "p"
func permutateWithChannel(channel chan<- []string,strings,prefix []string) {
    length := len(strings)
    if length == 0 {
        // Base case
        channel <- prefix
        return
    }
    // Recursive case
    newStrings := make([]string,length-1)
    for i,s := range strings {
        // Remove strings[i] and assign the result to newStringI
        // Append strings[i] to newPrefixI
        // Call the recursive case
        newStringsI := append(newStrings,strings[:i]...)
        newStringsI = append(newStringsI,strings[i+1:]...)
        newPrefixI := append(prefix,s)
        permutateWithChannel(channel,newStringsI,newPrefixI)
    }
}

// public,starts with uppercase "P"
func PermutateWithChannel(strings []string) chan []string {
    channel := make(chan []string)
    prefix := make([]string,len(strings))
    go func() {
        permutateWithChannel(channel,prefix)
        close(channel)
    }()
    return channel
}

以下是它的使用方法

// $src/main.go

package main

import (
    "./lib"
    "fmt"
)

var (
    fruits  = []string{"apple","banana","cherry","durian"}
    banned = "durian"
)

func main() {
    channel := lib.PermutateWithChannel(fruits)
    for myFruits := range channel {
        fmt.Println(myFruits)
        if myFruits[0] == banned {
            close(channel)
            //break
        }
    }
}

注意:

不需要break语句(上面评论),因为close(channel)导致range在下一次迭代中返回false,循环将终止.

问题

如果调用者不需要所有排列,则需要显式关闭()通道,否则在程序终止之前通道不会关闭(发生资源泄漏).另一方面,如果调用者需要所有排列(即范围循环直到结束),则调用者不得关闭()通道.这是因为close() – 已经关闭的通道会导致运行时混乱(见here in the spec).但是,如果确定它是否应该停止的逻辑并不像上面所示那么简单,我认为最好使用延迟关闭(通道).

问题

>实现这样的生成器的惯用方法是什么?
>惯用,谁应该负责关闭()频道 – 库函数调用者?
>如下所示修改我的代码是一个好主意,这样调用者有责任推迟关闭()无论如何?

在库中,修改

go func() {
        permutateWithChannel(channel,prefix)
        close(channel)
    }()

对此:

go permutateWithChannel(channel,prefix)

调用者中,修改

func main() {
    channel := lib.PermutateWithChannel(fruits)
    for myFruits := range channel {
        fmt.Println(myFruits)
        if myFruits[0] == banned {
            close(channel)
        }
    }
}

对此:

func main() {
    channel := lib.PermutateWithChannel(fruits)
    defer close(channel)    // <- Added
    for myFruits := range channel {
        fmt.Println(myFruits)
        if myFruits[0] == banned {
            break           // <- Changed
        }
    }
}

>尽管通过执行上面的代码无法观察到,并且算法的正确性不受影响,但在调用者close()s通道之后,运行库代码的goroutine在尝试发送到关闭的通道时应该会发生混乱.下一次迭代,如here in the spec所述,导致它终止.这会导致任何负面副作用吗?
>库函数的签名是func(strings [] string)chan [] string.理想情况下,返回类型应为< -chan []字符串,以将其限制为仅接收.但是,如果是负责关闭()频道的呼叫者,则无法将其标记为“仅接收”,因为close()内置函数不适用于仅接收频道.解决这个问题的惯用方法是什么?

I.替代品

前言:我将使用更简单的发电机,因为问题不涉及发电机的复杂性,而是发电机和消费者之间的信号,以及消费者自身的呼叫.这个简单的生成器只生成0到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

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

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

2.有渠道

如果您仍想使用频道,您可以.请注意,由于通道是由生成器创建的,并且由于消费者循环接收从通道接收的值(理想情况下使用for … range构造),因此生成器有责任关闭通道.与此相关也可以让您返回仅接收频道.

是的,关闭生成器中返回的通道最好作为延迟语句,因此即使生成器发生混乱,消费者也不会被阻止.但请注意,这个延迟关闭不在generate()函数中,而是在从generate()开始的匿名函数中执行,并作为新的goroutine执行;否则通道将在从generate()返回之前关闭 – 完全没用…

如果您想要从消费者发信号通知发生器(例如,中止并且不产生更多值),您可以使用例如另一个通道,传递给发电机.由于生成器只会“监听”此通道,因此它也可以声明为生成器的仅接收通道.如果您只需要发出一个事件的信号(在我们的情况下中止),则不需要在此通道上发送任何值,只需关闭即可.如果您需要发出多个事件的信号,可以通过在此通道上实际发送一个值来完成,即要执行的事件/操作(中止可能是多个事件中的一个).

您可以使用select statement作为惯用方法来处理返回通道上的发送值并观察传递给生成器的通道.

这是一个中止通道的解决方案:

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个goroutine(1个生成值,1个消耗/处理它们),并且很容易扩展它以使用任意数量的goroutine处理生成的值作为返回的通道发生器可以同时从多个goroutine中使用 – 通道可以安全地同时接收,数据竞争不会发生,设计;更多阅读:If I am using channels properly should I need to use mutexes?

II.未解决问题的答案

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

您没有观察到这一点,因为程序在主goroutine终止时终止(并且它不等待其他非主要goroutine完成 – 因此您的其他goroutines没有机会恐慌).见Spec: Program execution.

但是要知道panic()和recover()是出于特殊情况,它们并不适用于Java中的Exceptions和try-catch块等常规用例.应该避免恐慌,通过返回错误(并处理它们!),恐慌绝对不应该离开包的“边界”(例如panic()和recover()可能在包实现中被使用,但是恐慌状态应该被“抓住”在包内,而不是放出它).

猜你在找的Go相关文章