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 언어에 도입될 수도 있는 타입 파라미터에 대해 설명했습니다.
'Go' 카테고리의 다른 글
Go 언어 `reflect` 패키지 완벽 가이드: 런타임 타입 처리를 마스터하자 (0) | 2024.10.06 |
---|---|
Go 인터페이스의 모든 것: 내부 구조와 동작 원리 분석 (0) | 2024.10.06 |
Go 언어의 go:embed 완벽 가이드 - 사용법과 주의사항 (0) | 2024.10.06 |
Go 언어 GC 오버헤드 문제와 효과적인 해결 방안 (0) | 2024.10.06 |
Golang 멀티 모듈 리포지토리와 효과적인 버전 관리 전략 (0) | 2024.10.06 |