핵심 요약
Go 1.18에서 도입된 제네릭은 1.24에서 추론 성능과 타입 파라미터 사용성이 개선됐다. "어디에 쓰면 좋은가"의 실전 기준을 제시한다.
- 쓰는 곳: 컬렉션 헬퍼, 에러 래퍼, 리포지토리 인터페이스
- 쓰지 말 곳: 단순 도메인 모델, interface{} 대체만 하는 구간
- 원칙: "인스턴스화 횟수 ≥ 2, 타입별 로직이 동일"일 때만
실무 패턴 5가지
1) 컬렉션 헬퍼 (Map / Filter / Reduce)
func Map[T, U any](s []T, f func(T) U) []U {
r := make([]U, len(s))
for i, v := range s {
r[i] = f(v)
}
return r
}
// 사용
names := Map(users, func(u User) string { return u.Name })
2) Result[T] 타입
type Result[T any] struct {
Value T
Err error
}
func Ok[T any](v T) Result[T] { return Result[T]{Value: v} }
func Fail[T any](e error) Result[T] { return Result[T]{Err: e} }
3) 옵셔널 Option[T]
type Option[T any] struct {
value T
has bool
}
func Some[T any](v T) Option[T] { return Option[T]{v, true} }
func (o Option[T]) Get() (T, bool) { return o.value, o.has }
4) Repository 인터페이스
type Repository[T any, ID comparable] interface {
Find(ctx context.Context, id ID) (*T, error)
Save(ctx context.Context, e *T) error
}
5) 수치 제약(Constraints)
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](xs []T) T {
var s T
for _, x := range xs { s += x }
return s
}
안티패턴 4가지
1) interface{} 단순 치환
동적 디스패치가 필요한 곳에 제네릭을 쓰면 컴파일 시간만 늘어난다. 다형성은 interface로, 타입 안정성은 제네릭으로 구분.
2) 깊은 중첩 (Option[Result[Map[K, V]]])
타입 추론이 어려워지고 에러 메시지가 난해해진다. 최대 2단계까지.
3) 도메인 모델에 제네릭 박기
User[T]처럼 비즈니스 엔티티를 제네릭화하지 말 것. 한 타입으로 충분.
4) 함수 1회 호출에 제네릭
호출이 1번뿐이면 그냥 구체 타입을 쓰자. 유지보수성만 떨어진다.
1.24 개선점
- 타입 추론이 함수 리터럴 인자까지 확장 →
Map(xs, func(x) { return x.Name })형태 허용 - 제네릭 타입 별칭(type alias) 정식 지원
- 컴파일 속도 개선 (대형 코드베이스에서 체감)
자주 묻는 질문
런타임 오버헤드는?
Go 제네릭은 GCShape 기반으로 동일 레이아웃 타입은 하나의 인스턴스를 공유한다. 구체 타입 코드보다 미세하게 느릴 수 있으나 실무상 무시 가능.
interface와 제네릭 중 뭘 먼저?
다형성이면 interface, 타입 안정성이 핵심이면 제네릭. 두 개를 섞어 interface를 제약(constraint)으로 쓰는 패턴이 가장 강력하다.
댓글 0