제네릭(generics)(Go 1.18에 추가)은 타입 매개변수를 통해 여러 타입과 함께 동작하는 함수와 타입을 작성하게 해주며, 컴파일 시점 타입 안정성을 유지합니다. 타입마다 코드를 중복하거나 interface{}로 안정성을 잃는 옛 트레이드오프를 제거합니다.
제네릭이 해결하는 문제
{ ... }
{ ... }
{} { ... }
// T는 "순서가 있는"(< > 비교 가능) 것으로 제약된 타입 매개변수
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
Max(3, 5) // T는 int로 추론 → 5
Max(2.5, 1.1) // T는 float64로 추론 → 2.5
Max("a", "b") // T는 string으로 추론 → "b"
[T constraints.Ordered]는 제약(constraint)(어떤 타입이 허용되는지)을 가진 타입 매개변수 T를 선언합니다. 컴파일러가 인자에서 T를 추론합니다 — 하나의 구현이 여러 타입에 동작하며, 완전히 타입 안전합니다.
// 제약은 허용된 타입이나 필요한 메서드를 나열하는 interface
type Number interface {
~int | ~int64 | ~float64 // | 합집합은 "이들 중 아무것"을 의미
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n // 안전 — T는 수치 타입으로 제약됨
}
return total
}
Sum([]int{1, 2, 3}) // 6
Sum([]float64{1.5, 2.5}) // 4.0
// 내장 제약:
any // 모든 타입(interface{}의 별칭)
comparable // ==로 사용 가능한 타입(map 키 등)
// golang.org/x/exp/constraints: Ordered, Integer, Float 등
제약은 타입 매개변수에 어떤 연산이 유효한지 정의합니다(~는 "기반 타입이 이것인 모든 타입"을 의미).
// 모든 타입에 동작하는 제네릭 스택
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) { s.items = append(s.items, item) }
func (s *Stack[T]) Pop() T {
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item
}
intStack := Stack[int]{} // int의 스택
strStack := Stack[string]{} // string의 스택 — 같은 코드
✓ 제네릭 자료구조(스택, 트리, 집합), 컬렉션에 대한 유틸리티 함수
(Map, Filter, Reduce), 타입 안전 컨테이너
✗ 과용 금지 — interface나 구체 타입이 간단히 동작하면 그것을 선호.
Go 철학은 여전히 단순성을 선호; 제네릭은 도구이지 기본값이 아님.
제네릭은 실제 고충을 해결한 Go의 주요하고 오래 기다려온 추가(1.18)였습니다: 이전에는 여러 타입을 위한 재사용 코드 작성이 타입마다 중복(장황, 유지보수 불가)하거나 interface{}를 쓰는(컴파일 시점 타입 안정성 상실, 런타임 단언 필요) 것을 강요했습니다.
제네릭은 타입 안전하고 재사용 가능한 함수와 자료구조(컨테이너, Map/Filter/Reduce 같은 컬렉션 유틸리티)를 한 번 작성하게 하며, 컴파일러가 정확성을 강제하고 타입을 추론합니다.
타입 매개변수, 제약(any, comparable, 합집합/~ 문법 포함), 제네릭 타입을 이해하는 것은 생태계가 이를 채택하면서 점점 중요해집니다.
똑같이 중요한 것은 제네릭이 진정한 재사용을 위한 도구이지 기본값이 아니라는 Go 사고방식입니다 — 과용은 Go의 단순성 철학에 반합니다.
현대 Go에 대한 능숙함을 반영하는, 현재 자주 논의되는 주제입니다.