>  기사  >  백엔드 개발  >  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에서 반복자가 어떻게 작동하고 왜 그렇게 유용한지 살펴보는 것부터 시작하겠습니다.

간단한 변환: 아직 반복자가 없음

숫자 목록이 있고 각 숫자를 두 배로 늘리고 싶다고 상상해 보세요. 아래와 같은 간단한 기능을 사용하여 이 작업을 수행할 수 있습니다.

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

아주 간단하죠? 이는 모든 유형 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)
    }
}

이 경우 iterator가 훨씬 빠릅니다! 왜? 반복자는 전체 목록을 변환하지 않기 때문에 원하는 결과를 찾는 즉시 중지됩니다. 반면에 NormalTransform은 하나의 항목에만 관심이 있더라도 여전히 전체 목록을 변환합니다.

결론: 언제 Iterator를 사용해야 할까요?

그럼 Go에서 반복자를 사용하는 이유는 무엇인가요?

  • 효율성: 반복자는 필요하지 않은 경우 전체 목록을 처리하지 않음으로써 시간과 메모리를 모두 절약할 수 있습니다.
  • 유연성: 특히 데이터 스트림으로 작업하거나 조기에 중단해야 할 때 대규모 데이터 세트를 효율적으로 처리할 수 있습니다. 하지만 반복자는 이해하고 구현하기가 조금 더 까다로울 수 있다는 점을 명심하세요. 추가적인 성능 향상이 필요할 때, 특히 전체 목록을 미리 작업할 필요가 없는 시나리오에서 이를 사용하세요.

Iterator: 익숙해지면 빠르고 유연하며 재미있습니다!

위 내용은 Go의 반복자 이해하기: 재미있는 다이빙!의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.