Heim  >  Artikel  >  Backend-Entwicklung  >  Iteratoren in Go verstehen: Ein unterhaltsamer Tauchgang!

Iteratoren in Go verstehen: Ein unterhaltsamer Tauchgang!

Patricia Arquette
Patricia ArquetteOriginal
2024-10-25 02:28:02184Durchsuche

Understanding Iterators in Go: A Fun Dive!

Wenn Sie ein Go-Programmierer sind, haben Sie wahrscheinlich schon oft von Iteratoren in Go 1.22 und insbesondere in Go 1.23 gehört . Aber vielleicht zweifeln Sie immer noch am Kopf und fragen sich, warum sie nützlich sind oder wann Sie sie verwenden sollten. Dann sind Sie hier genau richtig! Schauen wir uns zunächst an, wie Iteratoren in Go funktionieren und warum sie so nützlich sein können.

Eine einfache Transformation: Noch kein Iterator

Stellen Sie sich vor, wir haben eine Liste mit Zahlen und möchten jede Zahl verdoppeln. Wir könnten dies mit einer einfachen Funktion wie der folgenden tun:

package main

import (
    "fmt"
)

func NormalTransform[T1, T2 any](list []T1, transform func(T1) T2) []T2 {
    transformed := make([]T2, len(list))

    for i, t := range list {
        transformed[i] = transform(t)
    }

    return transformed
}

func main() {
    list := []int{1, 2, 3, 4, 5}
    doubleFunc := func(i int) int { return i * 2 }

    for i, num := range NormalTransform(list, doubleFunc) {
        fmt.Println(i, num)
    }
}

Folgendes passiert, wenn Sie diesen Code ausführen:

0 2
1 4
2 6
3 8
4 10

Ganz einfach, oder? Dies ist eine grundlegende generische Go-Funktion, die eine Liste eines beliebigen Typs T1 übernimmt, eine Transformationsfunktion auf jedes Element anwendet und eine neue Liste mit der transformierten Liste eines beliebigen Typs T2 zurückgibt. Leicht zu verstehen, wenn Sie Go Generika!

kennen

Aber was wäre, wenn ich Ihnen sagen würde, dass es eine andere Möglichkeit gibt, damit umzugehen – die Verwendung eines Iterators?

Betreten Sie den Iterator!

Lassen Sie uns nun einen Blick darauf werfen, wie Sie einen Iterator für dieselbe Transformation verwenden können:

package main

import (
    "fmt"
)

func IteratorTransform[T1, T2 any](list []T1, transform func(T1) T2) iter.Seq2[int, T2] {
    return func(yield func(int, T2) bool) {
        for i, t := range list {
            if !yield(i, transform(t)) {
                return
            }
        }
    }
}

func main() {
    list := []int{1, 2, 3, 4, 5}
    doubleFunc := func(i int) int { return i * 2 }

    for i, num := range NormalTransform(list, doubleFunc) {
        fmt.Println(i, num)
    }
}

Bevor Sie es ausführen, müssen Sie sicherstellen, dass Ihre Go-Version 1.23 ist. Die Ausgabe ist genau die gleiche:

0 2
1 4
2 6
3 8
4 10

Aber Moment, warum sollten wir hier einen Iterator brauchen? Ist das nicht komplizierter? Schauen wir uns die Unterschiede genauer an.

Warum einen Iterator verwenden?

Auf den ersten Blick scheinen Iteratoren für etwas so Einfaches wie das Transformieren einer Liste etwas überentwickelt zu sein. Aber wenn Sie Benchmarks durchführen, beginnen Sie zu erkennen, warum sie eine Überlegung wert sind!

Lassen Sie uns beide Methoden vergleichen und sehen, wie sie funktionieren:

package main

import (
    "testing"
)

var (
    transform = func(i int) int { return i * 2 }
    list      = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
)

func BenchmarkNormalTransform(b *testing.B) {
    for i := 0; i < b.N; i++ {
        NormalTransform(list, transform)
    }
}

func BenchmarkIteratorTransform(b *testing.B) {
    for i := 0; i < b.N; i++ {
        IteratorTransform(list, transform)
    }
}

Hier ist das erste Benchmark-Ergebnis:

BenchmarkNormalTransform-8      41292933                29.49 ns/op
BenchmarkIteratorTransform-8    1000000000               0.3135 ns/op

Whoa! Das ist ein riesiger Unterschied! Aber Moment mal – hier herrscht ein wenig Ungerechtigkeit. Die NormalTransform-Funktion gibt eine vollständig transformierte Liste zurück, während die IteratorTransform-Funktion nur den Iterator einrichtet, ohne die Liste noch zu transformieren.

Machen wir es fair, indem wir den Iterator vollständig durchlaufen:

func BenchmarkIteratorTransform(b *testing.B) {
    for i := 0; i < b.N; i++ {
        for range IteratorTransform(list, transform) {
        }
    }
}

Jetzt sind die Ergebnisse vernünftiger:

BenchmarkNormalTransform-8      40758822                29.16 ns/op
BenchmarkIteratorTransform-8    53967146                22.39 ns/op

Okay, der Iterator ist etwas schneller. Warum? Denn NormalTransform erstellt eine gesamte transformierte Liste im Speicher (auf dem Heap), bevor es sie zurückgibt, während der Iterator die Transformation durchführt, während Sie sie durchlaufen, was Zeit und Speicher spart.

Lesen Sie hier mehr über Stack and Heap.

Die wahre Magie des Iterators entfaltet sich, wenn Sie nicht die gesamte Liste verarbeiten müssen. Vergleichen wir ein Szenario, in dem wir die Nummer 4 erst nach der Transformation der Liste finden möchten:

func BenchmarkNormalTransform(b *testing.B) {
    for i := 0; i < b.N; i++ {
        for _, num := range NormalTransform(list, transform) {
            if num == 4 {
                break
            }
        }
    }
}

func BenchmarkIteratorTransform(b *testing.B) {
    for i := 0; i < b.N; i++ {
        for _, num := range IteratorTransform(list, transform) {
            if num == 4 {
                break
            }
        }
    }
}

Die Ergebnisse sprechen für sich:

package main

import (
    "fmt"
)

func NormalTransform[T1, T2 any](list []T1, transform func(T1) T2) []T2 {
    transformed := make([]T2, len(list))

    for i, t := range list {
        transformed[i] = transform(t)
    }

    return transformed
}

func main() {
    list := []int{1, 2, 3, 4, 5}
    doubleFunc := func(i int) int { return i * 2 }

    for i, num := range NormalTransform(list, doubleFunc) {
        fmt.Println(i, num)
    }
}

In diesem Fall ist der Iterator viel schneller! Warum? Da der Iterator nicht die gesamte Liste transformiert, stoppt er, sobald er das gesuchte Ergebnis findet. Andererseits transformiert NormalTransform immer noch die gesamte Liste, auch wenn wir uns nur um ein Element kümmern.

Fazit: Wann werden Iteratoren verwendet?

Warum also einen Iterator in Go verwenden?

  • Effizienz: Iteratoren können sowohl Zeit als auch Speicher sparen, indem sie nicht die gesamte Liste verarbeiten, wenn Sie sie nicht benötigen.
  • Flexibilität: Sie ermöglichen Ihnen den effizienten Umgang mit großen Datensätzen, insbesondere wenn Sie mit Datenströmen arbeiten oder frühzeitig anhalten müssen. Bedenken Sie jedoch, dass Iteratoren etwas schwieriger zu verstehen und zu implementieren sein können. Verwenden Sie sie, wenn Sie eine zusätzliche Leistungssteigerung benötigen, insbesondere in Szenarien, in denen Sie nicht im Voraus mit einer vollständigen Liste arbeiten müssen.

Iteratoren: Sie sind schnell, flexibel und machen Spaß – wenn man erst einmal den Dreh raus hat!

Das obige ist der detaillierte Inhalt vonIteratoren in Go verstehen: Ein unterhaltsamer Tauchgang!. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn