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所述,导致它终止.这会导致任何负面副作用吗?
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()返回之前关闭 – 完全没用… 如果您想要从消费者发信号通知发生器(例如,中止并且不产生更多值),您可以使用例如另一个通道,传递给发电机.由于生成器只会“监听”此通道,因此它也可以声明为生成器的仅接收通道.如果您只需要发出一个事件的信号(在我们的情况下中止),则不需要在此通道上发送任何值,只需关闭即可.如果您需要发出多个事件的信号,可以通过在此通道上实际发送一个值来完成,即要执行的事件/操作(中止可能是多个事件中的一个). 您可以使用 这是一个中止通道的解决方案: 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()可能在包实现中被使用,但是恐慌状态应该被“抓住”在包内,而不是放出它). (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |