Go v1.18 제네릭 사용해보기

15731 단어 go
제네릭을 사용하면 모든 데이터 유형의 인수와 함께 사용할 수 있는 함수를 작성할 수 있습니다. 최신 버전의 Go는 제네릭을 언어에 도입했습니다. 따라서 이 기사에서는 Go에서 제네릭을 사용하는 방법을 살펴보고자 합니다.

이동 전 v1.18



이전 버전의 Go에는 제네릭이 없었습니다. 그러나 이전 버전의 Go에서 제네릭에 대한 한 가지 해결 방법은 유형 어설션 및 리플렉션과 함께 빈 인터페이스interface{}를 사용하는 것입니다. 예를 들어, 모든 유형의 배열을 취하고 배열을 합산하는 함수를 작성하고 싶다고 가정해 보겠습니다. 이전 버전의 Go에서는 다음과 같이 해야 합니다.

func sumAnyType(i interface{}) (o interface{}) {

    value := reflect.ValueOf(i)

    if reflect.TypeOf(i).Kind() != reflect.Slice || value.Len() < 1 {
        return
    }

    switch value.Index(0).Kind() {
    case reflect.Int:
        var res int = 0
        for i := 0; i < value.Len(); i++ {
            res += value.Index(i).Interface().(int)
        }
        return res
    case reflect.Float64:
        var res float64 = 0
        for i := 0; i < value.Len(); i++ {
            res += value.Index(i).Interface().(float64)
        }
        return res
    }

    return
}

func main() {

    log.Println(sumAnyType([]int{1, 2, 3})) // prints 6

    log.Println(sumAnyType([]float64{1.2, 2.5, 3.9})) // prints 7.6

    log.Println(sumAnyType([]float32{1.2, 2.5, 3.9})) // prints nil
}


위의 방법이 작동하지만 몇 가지 단점이 있는데 그 중 하나는 코드 반복입니다. 각 데이터 유형에 대한 합산 논리를 작성하고 있으며 특정 데이터 유형에 대한 논리를 작성하지 않으면 해당 유형의 배열을 합산할 수 없습니다.

Go v1.18 제네릭



최신 버전의 Go에 제네릭이 도입되면서 위의 예를 다음과 같이 단순화할 수 있습니다.

func sumAnyType[T int | float64](i []T) (o T) {
    for _, v := range i {
        o += v
    }
    return
}

func main() {

    log.Println(sumAnyType([]int{1, 2, 3})) // prints 6

    log.Println(sumAnyType([]float64{1.2, 2.5, 3.9})) // prints 7.6

    // log.Println(sumAnyType([]float32{1.2, 2.5, 3.9})) // does not compile
}


위에서 볼 수 있듯이 Go 제네릭은 각 데이터 유형에 대해 동일한 논리를 다시 작성할 필요 없이 여러 데이터 유형에 대해 동일한 함수를 사용할 수 있으므로 코드를 크게 단순화하고 DRY(반복 금지) 원칙을 적용합니다.

이제 Go에서 제네릭의 중요성을 이해했으므로 Go v1.18에서 제네릭에 도입된 다양한 기능을 이해해 보겠습니다.

1. any 키워드any 키워드는 빈 인터페이스인 interface{} 에 대한 별칭일 뿐입니다. 즉, interface{}로 할 수 있는 모든 작업은 any로도 할 수 있습니다. 예를 들어:

func printType(i any) {
    _, ok := i.(string)
    if ok {
        log.Println("its a string")
    }
    _, ok = i.(int)
    if ok {
        log.Println("its a integer")
    }
}


2. 유형 매개변수, 유형 인수 및 유형 제약
유형 매개변수는 함수가 제네릭이 되도록 허용하여 다른 유형의 인수와 함께 작동할 수 있도록 합니다. 유형 인수와 일반 함수 인수를 사용하여 함수를 호출합니다. 유형 제약 조건은 함수가 받을 수 있는 유형 목록을 정의합니다.


3. comparable 유형 비용 제약comparable== 또는 != 를 사용하여 비교할 수 있는 유형을 나타내는 Go v1.18의 사전 선언된 유형 제약 조건입니다.

4. 인터페이스로서의 타입 제약
형식 제약 조건은 재사용할 수 있도록 인터페이스로 선언할 수도 있습니다. 예를 들어:


type CustomConstraint interface {
    int | float64
}

func addOne[T CustomConstraint](i T) (o T) {
    o = i + 1
    return
}

func multiply2[T CustomConstraint](i T) (o T) {
    o = i * 2
    return
}

func main() {

    log.Println(addOne[int](1))
    log.Println(multiply2[float64](3.14))
}


5. ~ 키워드

기본 유형이 제약 조건과 동일한 모든 사용자 정의 유형을 제한하기 위해 유형 제약 조건에 접두사~를 붙일 수 있습니다.
CustomConstraint는 유형int만 제한하기 때문에 다음은 컴파일되지 않습니다.

type CustomInt int

type CustomConstraint interface {
    int
}

func addOne[T CustomConstraint](i T) (o T) {
    o = i + 1
    return
}

func main() {

    log.Println(addOne[CustomInt](1))
}


다음은 CustomConstraintint 및 기본 유형이 int인 다른 모든 사용자 정의 유형을 제한하기 때문에 성공적으로 컴파일됩니다.

type CustomInt int

type CustomConstraint interface {
    ~int
}

func addOne[T CustomConstraint](i T) (o T) {
    o = i + 1
    return
}

func main() {

    log.Println(addOne[CustomInt](1))
}


결론적으로 Go 제네릭은 특히 DRY를 시행하는 데 정말 유용하며 다양한 사용 사례에 사용할 수 있습니다. 제 생각에는 Go의 제네릭은 Java와 같은 다른 언어에 비해 더 간단하고 이해하기 쉽습니다.

추가 리소스:
  • https://go.dev/doc/tutorial/generics

  • https://teivah.medium.com/when-to-use-generics-in-go-36d49c1aeda
    - https://tutorialedge.net/golang/getting-starting-with-go-generics/
  • 좋은 웹페이지 즐겨찾기