Tutorial: Go 제네릭 시작하기

Tutorial: Go 제네릭 시작하기

Go는 한동안 제네릭 없이 지내다가 1.18에서 제네릭 지원을 추가했어요. 서로 다른 타입을 다루는 비슷한 함수를 여럿 만들지 않고, 하나의 제네릭 함수로 통합할 수 있다는 뜻이에요. 이 튜토리얼은 내장 제네릭의 기본기를 단계별로 보여줘요.

출처: Tutorial: Getting started with generics

제네릭 없이 두 개의 함수 만들기

우선 example/generics 모듈을 만들고 코드를 담아요.

mkdir generics
cd generics
$ go mod init example/generics
go: creating new go.mod: module example/generics

맵의 값들을 전부 더해 합계를 돌려주는 함수를 생각해 볼게요. 값 타입이 int64인 맵과 float64인 맵을 각각 처리하려면, 지금으로서는 두 함수가 필요해요.

// SumInts adds together the values of m.
func SumInts(m map[string]int64) int64 {
    var s int64
    for _, v := range m {
        s += v
    }
    return s
}

// SumFloats adds together the values of m.
func SumFloats(m map[string]float64) float64 {
    var s float64
    for _, v := range m {
        s += v
    }
    return s
}

이렇게 두 함수는 로직이 거의 같아요. 타입만 다를 뿐이죠.

하나의 제네릭 함수로 합치기

이제 두 함수를 하나의 제네릭 함수로 합쳐 볼게요. 제네릭 함수는 일반 함수 매개변수에 더해 **타입 매개변수(type parameter)**를 선언해요. 그리고 각 타입 매개변수는 어떤 타입 인자가 허용되는지를 정하는 **타입 제약(constraint)**을 가져요.

// SumIntsOrFloats sums the values of map m. It supports both int64 and float64
// as types for map values.
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}

이 함수는 두 개의 타입 매개변수 KV를 대괄호 안에 선언해요. K는 맵의 키 타입이고 comparable 제약을 줘요. comparable은 Go에 미리 선언된 제약으로, ==!= 비교 연산자에 쓸 수 있는 타입을 허용해요. 맵 키는 반드시 비교 가능해야 하므로 K를 이렇게 제약해 두는 거예요. V는 맵 값 타입이고 int64 | float64처럼 |로 허용 타입을 나열했어요.

호출하는 방식

호출할 때는 타입 인자를 명시할 수도 있고, 생략해 추론을 맡길 수도 있어요.

fmt.Printf("Generic Sums: %v and %v\n",
    SumIntsOrFloats[string, int64](ints),
    SumIntsOrFloats[string, float64](floats))

fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
    SumIntsOrFloats(ints),
    SumIntsOrFloats(floats))

타입 인자를 생략하면 컴파일러가 인자로부터 KV를 추론해요.

타입 제약 인터페이스로 정리하기

int64 | float64 같은 제약이 여기저기 반복되면 번거로워요. 그럴 땐 제약을 인터페이스로 뽑아 재사용할 수 있어요.

type Number interface {
    int64 | float64
}

이제 Number 제약을 쓰는 함수를 만들 수 있어요.

// SumNumbers sums the values of map m. It supports both integers
// and floats as map values.
func SumNumbers[K comparable, V Number](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}

주의할 점이 하나 있어요. 타입 매개변수는 제네릭 코드가 그 타입에 수행하는 모든 연산을 지원해야 해요. 예를 들어 숫자 타입만 허용하는 제약에 문자열 인덱싱 같은 연산을 하면 컴파일되지 않아요. 타입 인자가 제약이 허용하지 않는 타입이면 역시 컴파일 에러가 나요. 컴파일 타임에 제네릭 함수가 받을 수 있는 타입을 함수 하나에서 닫아 준다는 것, 그게 제네릭의 핵심이에요.

$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Generic Sums, type parameters inferred: 46 and 62.97
Generic Sums with Constraint: 46 and 62.97

더 알아보기