Iteratoren
In Go wird das Schlüsselwort for range für die Iteration über bestimmte Datenstrukturen verwendet. In den vorherigen Kapiteln wurden einige Anwendungen vorgestellt. Es kann nur auf wenige eingebaute Datenstrukturen angewendet werden:
- Arrays
- Slices
- Strings
- Maps
- Channels
- Ganzzahlwerte
Dies ist nicht sehr flexibel und bietet keine Erweiterbarkeit für benutzerdefinierte Typen. Seit Go 1.23 unterstützt das Schlüsselwort for range jedoch range over func, wodurch benutzerdefinierte Iteratoren möglich werden.
Einführung
Betrachten wir ein Beispiel zur Einführung in Iteratoren. Erinnern Sie sich an das Beispiel der Closure zur Berechnung der Fibonacci-Folge? Die Implementierung war:
func Fibonacci(n int) func() (int, bool) {
a, b, c := 1, 1, 2
i := 0
return func() (int, bool) {
if i >= n {
return 0, false
} else if i < 2 {
f := i
i++
return f, true
}
a, b = b, c
c = a + b
i++
return a, true
}
}Wir können dies in einen Iterator umwandeln:
func Fibonacci(n int) func(yield func(int) bool) {
a, b, c := 0, 1, 1
return func(yield func(int) bool) {
for range n {
if !yield(a) {
return
}
a, b = b, c
c = a + b
}
}
}Go-Iteratoren sind im range over func-Stil. Wir können direkt das Schlüsselwort for range verwenden:
func main() {
n := 8
for f := range Fibonacci(n) {
fmt.Println(f)
}
}Ausgabe:
0
1
1
2
3
5
8
13Wie oben gezeigt, ist ein Iterator eine Closure-Funktion, die eine Callback-Funktion als Parameter akzeptiert. Das yield ist nur eine Callback-Funktion, kein Schlüsselwort.
Push-Iteratoren
Die Definition von Iteratoren finden wir in der iter-Bibliothek:
Ein Iterator ist eine Funktion, die aufeinanderfolgende Elemente einer Sequenz an eine Callback-Funktion übergibt, konventionell yield genannt.
Ein Iterator ist also eine Funktion, die eine Callback-Funktion als Parameter akzeptiert und während der Iteration Elemente an yield übergibt.
Der Typ iter.Seq ist in der Standardbibliothek definiert:
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)Pull-Iteratoren
Pull-Iteratoren sind das Gegenteil von Push-Iteratoren. Der Benutzer kontrolliert die Iterationslogik und aktiviert die Sequenzelemente. Pull-Iteratoren haben typischerweise Funktionen wie next() und stop() zur Steuerung.
func Pull[V any](seq Seq[V]) (next func() (V, bool), stop func())
func Pull2[K, V any](seq Seq2[K, V]) (next func() (K, V, bool), stop func())Beispiel:
func main() {
n := 10
next, stop := iter.Pull(Fibonacci(n))
defer stop()
for {
fibn, ok := next()
if !ok {
break
}
fmt.Println(fibn)
}
}Fehlerbehandlung
Bei Fehlern während der Iteration können diese an yield übergeben werden:
func ScanLines(reader io.Reader) iter.Seq2[string, error] {
scanner := bufio.NewScanner(reader)
return func(yield func(string, error) bool) {
for scanner.Scan() {
if !yield(scanner.Text(), scanner.Err()) {
return
}
}
}
}Standardbibliothek
Viele Standardbibliotheken unterstützen nun Iteratoren:
slices.All
func All[Slice ~[]E, E any](s Slice) iter.Seq2[int, E]slices.Values
func Values[Slice ~[]E, E any](s Slice) iter.Seq[E]slices.Chunk
func Chunk[Slice ~[]E, E any](s Slice, n int) iter.Seq[Slice]maps.Keys
func Keys[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[K]maps.Values
func Values[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[V]Leistung
Da Go viele Optimierungen für Iteratoren durchgeführt hat, ist die Leistung nicht so gut wie bei nativen for range-Schleifen. Push-Iteratoren sind jedoch nicht weit von nativen Schleifen entfernt, während Pull-Iteratoren deutlich langsamer sind.
Zusammenfassung
Ähnlich wie bei Generika sind auch Go-Iteratoren umstritten. Einige argumentieren, dass Iteratoren zu viel Komplexität einführen und der Go-Philosophie der Einfachheit widersprechen. Rational betrachtet ermöglichen Iteratoren jedoch bequemeres Programmieren, insbesondere bei der Arbeit mit Slice-Typen, führen aber auch zu etwas mehr Komplexität. Insgesamt ist dies ein nützliches Feature.
