[注:私は GoのPythonスタイルのジェネレーター を読みましたが、これはそれの複製ではありません。 ]
Python/Ruby/JavaScript/ECMAScript 6では、ジェネレーター関数は、言語が提供するyield
キーワードを使用して記述できます。Goでは、ゴルーチンとチャネルを使用してシミュレートできます。
次のコードは、順列関数(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()
してはなりません(MUST NOT)。これは、すでに閉じられているチャネルをclose()
- ingするとランタイムパニックが発生するためです( 仕様のここで を参照)。ただし、停止する必要があるかどうかを判断するロジックが上記のように単純でない場合は、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()
sした後、ライブラリコードを実行するゴルーチンはpanic
を実行する必要があります文書化されているように、次の反復でクローズドチャネルに送信します ここでは仕様 。これにより悪影響が生じますか?func(strings []string) chan []string
です。理想的には、戻り値の型を_<-chan []string
_にして、受信専用に制限する必要があります。ただし、チャネルのclose()
を担当するのが呼び出し側である場合、close()
組み込み関数が機能しないため、「受信専用」としてマークできませんでした。受信専用チャネル。これに対処する慣用的な方法は何ですか?まえがき:問題はジェネレーターの複雑さではなく、ジェネレーターとコンシューマー間の信号、および呼び出しに関係しないため、私ははるかに単純なジェネレーターを使用します消費者自身の。この単純なジェネレータは、_0
_から_9
_までの整数を生成するだけです。
単純なconsumer関数を渡すと、generate-consumerパターンがよりクリーンになり、中絶やその他のアクションが必要な場合にシグナル値を返すことができるという利点もあります。
また、例では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しか使用しないことです。
それでもチャンネルでやりたい場合は、可能です。チャネルはジェネレーターによって作成され、コンシューマーはチャネルから受信した値をループするため(理想的には_for ... range
_構文を使用して)、チャネルを閉じるのはジェネレーターの責任であることに注意してください。これで解決すると、受信専用チャネルを返すこともできます。
そして、はい、返されたチャネルをジェネレータで閉じるのは遅延ステートメントとして行うのが最善です。そのため、ジェネレータがパニックになっても、コンシューマはブロックされません。ただし、この遅延クローズはgenerate()
関数ではなく、generate()
から開始され、新しいgoroutineとして実行される無名関数にあることに注意してください。そうでない場合、チャネルはgenerate()
から返される前に閉じられます-まったく役に立ちません...
そして、コンシューマーからジェネレーターに信号を送りたい場合(例えば、中止してそれ以上の値を生成しない場合)は、例えばジェネレータに渡される別のチャネル。ジェネレータはこのチャネルのみを「リッスン」するため、ジェネレータへの受信専用チャネルとして宣言することもできます。 1つのイベント(この場合は打ち切り)を通知するだけでよく、このチャネルで値を送信する必要がない場合は、単純なクローズでそれを行います。複数のイベントを通知する必要がある場合、実際にこのチャネルに値を送信することで実行できます。実行するイベント/アクションです(中止は複数のイベントからの1つです)。
そして 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つのゴルーチン(1つは値を生成し、1つはそれらを消費/処理する)をすでに使用していることであり、それを拡張して、任意の数のゴルーチンを使用して生成された値を処理することは、ジェネレータは複数のゴルーチンから同時に使用できます-チャネルは同時に受信しても安全です。設計上、データの競合は発生しません。続きを読む: チャネルを適切に使用している場合、ミューテックスを使用する必要がありますか?
Goroutineで「キャッチされていない」パニックが発生すると、goroutineの実行は終了しますが、リソースリークに関して問題は発生しません。しかし、別のgoroutineとして実行された関数が、パニックでない場合に割り当てられたリソース(非据え置きステートメント内)を解放すると、そのコードは明らかに実行されず、たとえばリソースリークが発生します。
メインのゴルーチンが終了するとプログラムが終了するので、これを観察していません(他の非メインゴルーチンが終了するのを待たないため、他のゴルーチンはパニックする機会がありませんでした)。 仕様:プログラムの実行 を参照してください。
ただし、panic()
およびrecover()
は例外的なケースを対象としているため、JavaのExceptionsブロックや_try-catch
_ブロックなどの一般的なユースケースは対象としていません。たとえば、エラーを返す(そしてエラーを処理する)ことでパニックを回避する必要があります。パニックは、パッケージの「境界」を残さないようにする必要があります(たとえば、panic()
およびrecover()
は、パッケージの実装で使用されますが、パニック状態はパッケージ内で「キャッチ」され、パッケージから解放されません)。
私の考えでは、通常、ジェネレーターは内部的にクロージャーのラッパーにすぎません。このようなもの
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
Iczaの答えに同意します。要約すると、2つの選択肢があります。
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))
}
}
_