숏컷 코드
func First[T any](items []T) (T, bool) {
if len(items) == 0 {
var zero T
return zero, false
}
return items[0], true
}Go 제네릭은 같은 알고리즘을 여러 타입에 적용하되 타입 안정성은 유지하고 싶을 때 씁니다.
문법
타입 매개변수는 함수 이름 뒤에 둔다
func Identity[T any](v T) T {
return v
}[T any]에서 T는 타입 매개변수이고, any는 어떤 타입이든 받을 수 있다는 constraint입니다.
constraint는 가능한 연산을 제한한다
import "cmp"
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}> 비교를 하려면 모든 타입을 받을 수 없습니다. cmp.Ordered처럼 비교 가능한 타입으로 constraint를 좁히면 함수 안에서 사용할 수 있는 연산도 분명해집니다.
타입도 generic으로 만들 수 있다
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}컨테이너, 캐시, 결과 래퍼처럼 내부 값 타입만 달라지고 구조가 같은 경우 generic type이 잘 맞습니다.
선택 기준
| 상황 | 선택 |
|---|---|
| 같은 알고리즘이 타입만 다름 | 제네릭 함수 |
| 컨테이너 구조가 타입만 다름 | 제네릭 타입 |
| 타입별 동작이 크게 다름 | 일반 함수나 인터페이스 |
| 런타임 타입 분기가 핵심 | 제네릭보다 interface/type switch |
| 중복이 적고 읽기 쉬움 | 굳이 제네릭으로 묶지 않음 |
Go 제네릭은 추상화를 늘리는 기능입니다. 중복 제거보다 호출부와 오류 메시지가 더 읽기 쉬워지는지까지 같이 봐야 합니다.
주의할 점
any를 붙였다고 함수 안에서 모든 연산을 할 수 있는 것은 아닙니다. constraint가 허용한 연산만 사용할 수 있습니다. 또한 제네릭은 런타임 타입 정보를 자동으로 풍부하게 만들어 주는 기능이 아니므로, 타입별 분기가 핵심이면 인터페이스나 명시적인 분기를 검토하는 편이 낫습니다.
참고 링크
3 sources