Go

Go 언어 제네릭 완벽 정리: 타입 파라미터와 인터페이스 활용법

드리프트2 2024. 10. 6. 22:01

Go 언어 제네릭 완벽 정리: 타입 파라미터와 인터페이스 활용법

.

왜 제네릭이 필요한가요?

Go 언어에서는 오랫동안 제네릭을 추가해달라는 요청이 많았고, 여러 디자인이 검토되었습니다.

 

제네릭의 필요성은 다음과 같은 코드를 타입에 의존하지 않고 작성할 수 있게 해줍니다.

func PrintInts(s []int) {
    for _, v := range s {
        fmt.Print(v)
    }
}

func PrintStrings(s []string) {
    for _, v := range s {
        fmt.Print(v)
    }
}

 

제네릭을 사용하면 다음과 같이 작성할 수 있습니다.

func Print[T any](s []T) {
    for _, v := range s {
        fmt.Print(v)
    }
}

 

여기서 T는 타입 파라미터로, 함수 사용 시 구체적인 타입이 인수로 전달됩니다.

 

따라서 함수는 임의의 타입의 슬라이스를 인수로 받을 수 있습니다.

 

이번 글에서는 Type Parameters - Draft Design에서 제안된 타입 파라미터에 대해 간단히 설명합니다.

 

이 드래프트 디자인에는 다음과 같은 내용이 포함되어 있습니다.

  • 타입 파라미터 도입
  • 타입 인수와 인스턴스화
  • 인터페이스를 통한 타입 파라미터 제한
  • 인터페이스 내 타입 리스트
  • go2go를 사용한 타입 파라미터 실험

 

go2go로 제네릭 실험하기

드래프트 디자인만으로는 이해하기 어려울 수 있습니다.

 

그래서 Go 팀은 커뮤니티의 피드백을 받기 위해 go2go라는 커맨드라인 도구를 제공하고 있습니다.

 

go2go는 타입 파라미터를 포함한 코드를 현재의 Go 컴파일러로 변환하여 빌드할 수 있게 해줍니다.

 

go2go는 Go의 툴체인을 브랜치로 전환하고, Go 컴파일러를 빌드한 후 해당 컴파일러로 소스 코드를 빌드하는 방식으로 사용됩니다.

 

go2go Playground를 사용하면 웹에서 쉽게 타입 파라미터를 실험할 수 있습니다.

 

제네릭 함수 정의하기

패키지 스코프에서 정의된 함수에는 타입 파라미터를 설정할 수 있습니다.

 

이런 함수를 제네릭 함수라고 부릅니다.

 

함수명 뒤에 타입 파라미터 리스트를 작성합니다.

 

T는 임의의 타입을 받는다는 것을 의미합니다.

func Print[T any](s []T) {
    for _, v := range s {
        fmt.Print(v)
    }
}

func main() {
    Print[string]([]string{"Hello, ", "playground\n"})
}

 

타입 인수로 인스턴스화된 함수에서는 타입 파라미터가 타입으로 처리됩니다.

 

함수의 인수는 []T, 변수는 T 타입입니다.

 

타입 추론

실제 인수의 타입에 따라 타입 인수를 추론할 수 있는 경우, 타입 인수를 생략할 수 있습니다.

func main() {
    Print([]string{"Hello, ", "playground\n"})
    Print([]int{1, 2, 3})
}

 

첫 번째 호출은 Print 함수가 string 타입으로 추론되고, 두 번째 호출은 int 타입으로 추론됩니다.

 

제네릭 타입

타입 정의에서도 타입 파라미터를 사용할 수 있습니다.

 

예를 들어, 임의의 타입 슬라이스를 기본형으로 하는 타입을 정의할 수 있습니다.

 

type List[T any] []T

var ns List[int] = List[int]{10, 20, 30}
type IntList = List[int]
ms := IntList{100, 200, 300}

 

제네릭 타입은 타입 인수로 인스턴스화하여 사용할 수 있습니다.

 

타입 추론은 현재 드래프트 디자인에서는 지원되지 않습니다.

 

인터페이스를 통한 제한

제네릭 함수나 타입에서는 타입 인수로 아무 타입이나 지정할 수 있다면 불편할 수 있습니다.

 

인터페이스를 통해 타입 파라미터에 제한을 두면 더 유용합니다.

type Hex int
func (h Hex) String() string {
    return fmt.Sprintf("%x", int(h))
}

func PrintStringers[T fmt.Stringer](s []T) {
    for _, v := range s {
        fmt.Printf("0x%s\n", v.String())
    }
}

func main() {
    PrintStringers([]Hex{100, 200})
}

 

PrintStringers 함수는 fmt.Stringer 인터페이스를 구현한 타입만 타입 인수로 받을 수 있습니다.

 

인터페이스의 타입 리스트

기존 인터페이스는 메서드에 의한 제한만 가능했습니다.

 

연산자를 사용한 제네릭 처리가 필요할 때는 불편합니다.

 

드래프트 디자인에서는 인터페이스 정의에 타입 리스트를 추가할 수 있게 제안하고 있습니다.

type addable interface {
    type int, int8, int16, int32, int64, uint, uint8,
    uint16, uint32, uint64, uintptr,
    float32, float64, complex64, complex128, string
}

func sum[T addable](x []T) T {
    var total T
    for _, v := range x {
        total += v
    }
    return total
}

func main() {
    fmt.Println(sum([]int{1, 2, 3}))
    fmt.Println(sum([]string{"Hello, ", "World"}))
}

 

addable 인터페이스는 나열된 타입이나 그 타입을 기본형으로 가지는 타입만 타입 인수로 지정할 수 있습니다.

 

즉, sum 함수는 덧셈이 가능한 타입만 처리할 수 있습니다.

 

앞으로의 전망

드래프트 디자인이 채택된다면, 비교 가능한 타입을 표현하는 comparable 인터페이스 같은 내장 인터페이스가 추가되거나, 표준 패키지에 변화가 생길 것입니다.

 

예를 들어, bytes, strings, slices 패키지의 제네릭화 등이 예상됩니다.

 

예를 들어, comparable 인터페이스가 추가되면 다음과 같이 임의의 맵 키를 가져오는 함수가 쉽게 정의될 수 있습니다.

func Keys[K comparable, V any](m map[K]V) []K {
    keys := make([]K, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    return keys
}

func main() {
    keys := Keys[int, string](map[int]string{1: "one", 2: "two"})
    fmt.Println(keys)
}

 

 

결론

이번 글에서는 Go 언어에 도입될 수도 있는 타입 파라미터에 대해 설명했습니다.