[注意:我读过Python-style generators in Go,这不是它的重复。 ]
在Python / Ruby / JavaScript / ECMAScript 6中,可以使用语言提供的yield
关键字编写生成器函数。在Go中,可以使用goroutine和频道进行模拟。
以下代码显示了如何实现置换函数(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, 0, 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, 0, len(strings))
go func() {
permutateWithChannel(channel, strings, 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()
通道,否则在程序终止之前通道将不会关闭(发生资源泄漏)。另一方面,如果调用者需要所有排列(即range
循环直到结束),则调用者不得close()
该通道。这是因为close()
- 已经关闭的通道导致运行时恐慌(请参阅here in the spec)。但是,如果确定是否应该停止的逻辑并不像上面所示那么简单,我认为最好使用defer close(channel)
。
close()
频道 - 图书馆功能或来电者负责?defer close()
频道负责吗?在库中,修改:
go func() {
permutateWithChannel(channel, strings, prefix)
close(channel)
}()
到此:
go permutateWithChannel(channel, strings, 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()
的通道之后,运行库代码的goroutine应该panic
当它尝试在下一次迭代中发送到关闭的通道,如文档here in the spec所示,导致它终止。这会导致任何负面副作用吗?func(strings []string) chan []string
。理想情况下,返回类型应为<-chan []string
,以将其限制为仅接收。但是,如果调用者对close()
频道负责,则无法将其标记为&#34;仅接收&#34;,因为close()
内置函数不起作用在仅接收频道上。解决这个问题的惯用方法是什么?答案 0 :(得分:20)
前言:我将使用更简单的生成器,因为问题不涉及生成器的复杂性,而是发生器和消费者之间的信号,以及消费者本身的调用。这个简单的生成器只生成从0
到9
的整数。
通过简单的消费者功能,生成 - 消费者模式更加清晰,如果需要堕胎或任何其他行动,它还具有返回值信号的优势。
因为在示例中只有一个事件要发出信号(“abort”),所以消费者函数将具有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来生成和消耗值。
如果您仍想使用频道,则可以。请注意,由于通道是由生成器创建的,并且由于消费者循环接收从通道接收的值(理想情况下使用for ... range
构造),因此关闭通道是生成器的责任。与此相关也可以让您返回仅接收频道。
是的,关闭生成器中返回的通道最好作为延迟语句,因此即使生成器发生混乱,消费者也不会被阻止。但请注意,此延迟关闭不在generate()
函数中,而是在从generate()
开始的匿名函数中执行,并作为新的goroutine执行;否则频道会在从generate()
返回之前关闭 - 完全没用...
如果您想要从消费者发信号通知发生器(例如,中止而不生成更多值),您可以使用例如另一个通道,传递给发电机。由于生成器只会“监听”此通道,因此它也可以声明为生成器的仅接收通道。如果您只需要发出一个事件的信号(在我们的情况下中止),则不需要在此通道上发送任何值,只需关闭即可。如果您需要发出多个事件的信号,可以通过在此频道上实际发送一个值来完成,即要执行的事件/操作(中止可能来自多个事件)。
您可以使用select
statement作为惯用方法来处理在返回的频道上发送值并观看传递给发生器的频道。
以下是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个goroutine(1个生成值,1个消耗/处理它们),并且很容易扩展它以使用任意数量的goroutine处理生成的值作为生成器返回的通道可以同时从多个goroutine中使用 - 通道可以安全地同时接收,数据竞争不会发生,设计;更多阅读:If I am using channels properly should I need to use mutexes?
goroutine上的“未捕获”恐慌将结束goroutine的执行,但不会导致资源泄漏问题。但是,如果作为单独的goroutine执行的函数在非恐慌的情况下释放由它分配的资源(在非延迟语句中),那么该代码显然不会运行并且例如会导致资源泄漏。
你没有观察到这一点,因为程序在主goroutine终止时终止(并且它不等待其他非主要goroutine完成 - 所以你的其他goroutines没有机会恐慌)。请参阅Spec: Program execution。
但是要知道panic()
和recover()
用于例外情况,它们不适用于Java中的异常和try-catch
块等常规用例。应该避免恐慌,例如,通过返回错误(并处理它们!),恐慌绝对不应该离开包的“边界”(例如panic()
和recover()
可能被证明可以用于包的实现,但恐慌状态应该被“抓住”在包内,而不是放弃它。)
答案 1 :(得分:3)
在我看来,通常生成器只是内部封闭的封装器。像这样的东西
package main
import "fmt"
// This function `generator` returns another function, which
// we define anonymously in the body of `generator`. The
// returned function _closes over_ the variable `data` to
// form a closure.
func generator(data int, permutation func(int) int, bound int) func() (int, bool) {
return func() (int, bool) {
data = permutation(data)
return data, data < bound
}
}
// permutation function
func increment(j int) int {
j += 1
return j
}
func main() {
// We call `generator`, assigning the result (a function)
// to `next`. This function value captures its
// own `data` value, which will be updated each time
// we call `next`.
next := generator(1, increment, 7)
// See the effect of the closure by calling `next`
// a few times.
fmt.Println(next())
fmt.Println(next())
fmt.Println(next())
// To confirm that the state is unique to that
// particular function, create and test a new one.
for next, generation, ok := generator(11, increment, 17), 0, true; ok; {
generation, ok = next()
fmt.Println(generation)
}
}
它看起来不像'范围'那么优雅,但在语义和句法上对我来说非常清晰。它有效http://play.golang.org/p/fz8xs0RYz9
答案 2 :(得分:1)
我同意icza的回答。总而言之,有两种选择:
func myIterationFn(
的 yield
强> func (myType)) (stopIterating bool)
。这样做的缺点是将控制流程转为myGenerator
功能。 myIterationFn
不是Pythonic生成器,因为它不会返回可迭代序列。myIterationFn
转换为返回可迭代序列的函数。以下代码提供了此类转换的示例。myMapper := func(yield func(int) bool) {
for i := 0; i < 5; i++ {
if done := yield(i); done {
return
}
}
}
iter, cancel := mapperToIterator(myMapper)
defer cancel() // This line is very important - it prevents goroutine leaks.
for value, ok := iter(); ok; value, ok = iter() {
fmt.Printf("value: %d\n", value)
}
这是一个完整的程序作为例子。 mapperToIterator
执行从映射函数到生成器的转换。 Go缺乏泛型需要从interface{}
转换为int
。
package main
import "fmt"
// yieldFn reports true if an iteration should continue. It is called on values
// of a collection.
type yieldFn func(interface{}) (stopIterating bool)
// mapperFn calls yieldFn for each member of a collection.
type mapperFn func(yieldFn)
// iteratorFn returns the next item in an iteration or the zero value. The
// second return value is true when iteration is complete.
type iteratorFn func() (value interface{}, done bool)
// cancelFn should be called to clean up the goroutine that would otherwise leak.
type cancelFn func()
// mapperToIterator returns an iteratorFn version of a mappingFn. The second
// return value must be called at the end of iteration, or the underlying
// goroutine will leak.
func mapperToIterator(m mapperFn) (iteratorFn, cancelFn) {
generatedValues := make(chan interface{}, 1)
stopCh := make(chan interface{}, 1)
go func() {
m(func(obj interface{}) bool {
select {
case <-stopCh:
return false
case generatedValues <- obj:
return true
}
})
close(generatedValues)
}()
iter := func() (value interface{}, notDone bool) {
value, notDone = <-generatedValues
return
}
return iter, func() {
stopCh <- nil
}
}
func main() {
myMapper := func(yield yieldFn) {
for i := 0; i < 5; i++ {
if keepGoing := yield(i); !keepGoing {
return
}
}
}
iter, cancel := mapperToIterator(myMapper)
defer cancel()
for value, notDone := iter(); notDone; value, notDone = iter() {
fmt.Printf("value: %d\n", value.(int))
}
}