[Примечание: я читаю генераторы стиля Python в 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()
-в уже закрытом канале вызывает панику (см. здесь в спецификации). Однако, если логика, чтобы определить, должна ли она остановиться или нет, не так проста, как показано выше, я думаю, что лучше использовать 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
, когда он пытается отправить на замкнутый канал на следующей итерации, как описано здесь, в спецификации, в результате чего он заканчивается. Означает ли это какой-либо негативный побочный эффект? - Подпись библиотечной функции
func(strings []string) chan []string
. В идеале тип возврата должен быть<-chan []string
, чтобы ограничить его только получением. Однако если вызывающий абонент несет ответственность заclose()
за канал, он не может быть помечен как "только для приема", так как встроенная функцияclose()
не работает в каналах только для приема. Каков идиоматический способ справиться с этим?