[ Note: I read Python-style generators in Go, this is not a duplicate of it. ]
In Python / Ruby / JavaScript / ECMAScript 6, generator functions could be written using the yield
keyword provided by the language. In Go, it could be simulated using a goroutine and a channel.
The Code
The following code shows how a permutation function (abcd, abdc, acbd, acdb, ..., dcba) could be implemented:
// $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
}
Here is how it could be used:
// $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
}
}
}
Note:
The break
statement (commented above) is not needed, as close(channel)
causes range
to return false
in the next iteration, the loop will terminate.
The Problem
If the caller doesn't need all the permutations, it needs to close()
the channel explicitly, or the channel will not be closed until the program terminates (resource leakage occurs). On the other hand, if the caller needs all the permutations (i.e. the range
loops until the end), the caller MUST NOT close()
the channel. It is because close()
-ing an already closed channel causes a runtime panic (see here in the spec). However, if the logic to determine whether it should stop or not is not as simple as shown above, I think it is better to use defer close(channel)
.
The Questions
- What is the idiomatic way to implement generators like this?
- Idiomatically, who should be responsible to
close()
the channel - the library function or the caller?
- Is it a good idea to modify my code like below, so that the caller is responsible to
defer close()
the channel no matter what?
In the library, modify this:
go func() {
permutateWithChannel(channel, strings, prefix)
close(channel)
}()
to this:
go permutateWithChannel(channel, strings, prefix)
In the caller, modify this:
func main() {
channel := lib.PermutateWithChannel(fruits)
for myFruits := range channel {
fmt.Println(myFruits)
if myFruits[0] == banned {
close(channel)
}
}
}
to this:
func main() {
channel := lib.PermutateWithChannel(fruits)
defer close(channel) // <- Added
for myFruits := range channel {
fmt.Println(myFruits)
if myFruits[0] == banned {
break // <- Changed
}
}
}
- Despite it is not observable by executing the code above, and the correctness of the algorithm is not affected, after the caller
close()
s the channel, the goroutine running the library code should panic
when it tries to send to the closed channel in the next iteration, as documented here in the spec, causing it to terminate. Does this cause any negative side-effect?
- The signature of the library function is
func(strings []string) chan []string
. Ideally, the return type should be <-chan []string
to restrict it to be receive-only. However, if it is the caller who is responsible to close()
the channel, it could not be marked as "receive-only", as the close()
built-in function does not work on receive-only channels. What is the idiomatic way to deal with this?
See Question&Answers more detail:
os