ホームページ  >  記事  >  バックエンド開発  >  Go のイテレータを理解する: 楽しいダイビング!

Go のイテレータを理解する: 楽しいダイビング!

Patricia Arquette
Patricia Arquetteオリジナル
2024-10-25 02:28:02184ブラウズ

Understanding Iterators in Go: A Fun Dive!

Go プログラマーであれば、おそらく Go 1.22、特に Go 1.23 でイテレーターについて何度も聞いたことがあるでしょう。 。しかし、なぜそれらが役立つのか、いつ使用する必要があるのか​​、まだ頭を悩ませているかもしれません。そうですね、あなたは正しい場所にいます!まずは、Go でイテレータがどのように機能するのか、そしてなぜイテレータが非常に役立つのかを見てみましょう。

単純な変換: イテレータはまだありません

数値のリストがあり、それぞれの数値を 2 倍にしたいと想像してください。以下のような簡単な関数を使用してこれを行うことができます:

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)
    }
}

このコードを実行すると、次のことが起こります:

0 2
1 4
2 6
3 8
4 10

とてもシンプルですよね?これは基本的な汎用 Go 関数で、任意の型 T1 のリストを受け取り、各要素に変換関数を適用して、任意の型 T2 の変換されたリストを含む新しいリストを返します。 Goジェネリックを知っていれば簡単に理解できます!

しかし、これを処理する別の方法、つまりイテレータを使用する方法があると言ったらどうなるでしょうか?

イテレータを入力してください!

ここで、同じ変換にイテレータを使用する方法を見てみましょう:

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)
    }
}

実行する前に、Go のバージョンが 1.23 であることを確認する必要があります。出力はまったく同じです:

0 2
1 4
2 6
3 8
4 10

しかし、待ってください、なぜここでイテレータが必要なのでしょうか?それはもっと複雑ではないでしょうか?違いを詳しく見てみましょう。

イテレータを使用する理由

一見すると、イテレーターは、リストの変換のような単純なことに対して少し過剰設計されているように見えます。しかし、ベンチマークを実行すると、ベンチマークを検討する価値がある理由が分かり始めます!

両方のメソッドをベンチマークして、そのパフォーマンスを確認してみましょう:

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)
    }
}

最初のベンチマーク結果は次のとおりです:

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

おお!それは大きな違いです!しかし、ちょっと待ってください。ここには少し不公平があります。 NormalTransform 関数は完全に変換されたリストを返しますが、IteratorTransform 関数はリストをまだ変換せずに反復子を設定するだけです。

イテレータを完全にループして公平にしましょう:

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

結果はより合理的になりました:

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

わかりました、イテレーターは少し高速です。なぜ? NormalTransform は、変換されたリスト全体をメモリ (ヒープ上) に作成してから返しますが、反復子はループ処理中に変換を実行するため、時間とメモリが節約されます。

スタックとヒープ について詳しくは、こちらをご覧ください

イテレータの本当の魔法は、リスト全体を処理する必要がないときに起こります。リストを変換した後に数字の 4 だけを見つけたいというシナリオのベンチマークを行ってみましょう:

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
            }
        }
    }
}

結果がすべてを物語っています:

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)
    }
}

この場合、反復子ははるかに高速です。なぜ?イテレーターはリスト全体を変換するわけではないため、探している結果が見つかるとすぐに停止します。一方、NormalTransform は、たとえ 1 つの項目だけを対象としても、リスト全体を変換します。

結論: イテレータをいつ使用するか?

それでは、なぜ Go でイテレータを使用するのでしょうか?

  • 効率: イテレータは、必要がない場合にリスト全体を処理しないことで、時間とメモリの両方を節約できます。
  • 柔軟性: 特にデータのストリームを扱う場合や早期に停止する必要がある場合に、大規模なデータセットを効率的に処理できるようになります。 ただし、イテレーターは理解して実装するのが少し難しい場合があることに注意してください。追加のパフォーマンス向上が必要な場合、特にリスト全体を事前に処理する必要がないシナリオでこれらを使用してください。

イテレーター: コツを掴めば、高速かつ柔軟で楽しいです!

以上がGo のイテレータを理解する: 楽しいダイビング!の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。