제네릭 함수
타입 매개변수는 일반 매개변수 앞의 대괄호 안에 씁니다. 각 타입 매개변수에는 허용되는 타입을 정하는 제약이 있습니다.
T와 U는 인자로부터 추론되므로 Map[int, string](...)이라고 쓸 일은 거의 없습니다. 추론은 함수 인자에서 이루어지며, 결과에만 나타나는 타입은 추론할 수 없습니다. func Zero[T any]() T라면 Zero[int]()라고 써야 합니다.
Go 1.18 이전에는 이 함수를 타입마다 한 번씩 작성하거나, []interface{}를 받으면서 타입 안전성을 포기해야 했습니다.
제약
제약은 인터페이스입니다. 쓸 수 있는 타입을 제한하고, 그 대가로 그 타입의 값에 허용되는 연산을 컴파일러에 알려 줍니다.
| 제약 | 허용하는 타입 | 쓸 수 있는 것 |
|---|---|---|
any | 모든 타입 | 대입, 전달, 저장 |
comparable | ==를 지원하는 타입 | ==, !=, 맵 키 |
cmp.Ordered (Go 1.21) | 정수, 실수, 문자열 | <, <=, >, >=, 그리고 == |
| 메서드가 있는 인터페이스 | 그 메서드를 가진 타입 | 메서드 호출 |
~int | ~float64 같은 타입 유니온 | 정확히 그 타입들 | 모든 타입이 지원하는 연산자 |
comparable 대신 any를 쓰면 v == target이 컴파일되지 않습니다: invalid operation: v == target (incomparable types in type set). 연산자를 합법으로 만드는 것은 제약입니다.
이 두 헬퍼는 이미 slices.Index와 slices.Max로 존재합니다. 표준 slices, maps, cmp 패키지가 일상적인 제네릭 수요 대부분을 다루므로, 직접 만들기 전에 먼저 확인하세요.
타입 유니온과 ~로 만드는 커스텀 제약
제약은 |로 타입을 나열할 수 있습니다. 허용되는 연산자는 나열된 모든 타입이 지원하는 것입니다.
물결표가 중요합니다. ~int64는 "기반 타입이 int64인 모든 타입"을 뜻하므로 이름 있는 타입 Cents도 받아들여집니다. 그냥 int64라고 쓰면 Sum([]Cents{...})은 Cents does not satisfy Number (possibly missing ~ for int64 in Number)로 실패합니다.
타입 유니온을 담은 인터페이스는 제약으로만 쓸 수 있고, 일반 변수 타입으로는 쓸 수 없습니다. var n Number는 컴파일되지 않습니다.
golang.org/x/exp/constraints 패키지에는 Integer, Float, Signed 같은 기성 제약이 있습니다. 표준 라이브러리 밖에 있어서 여기 실행기에서는 import할 수 없으며, 표준 제약은 cmp.Ordered입니다.
제네릭 타입
구조체, 슬라이스, 맵 같은 타입도 타입 매개변수를 가질 수 있습니다. 제네릭 타입의 메서드는 리시버의 매개변수를 씁니다.
이 예제의 두 가지 세부 사항은 계속 등장합니다:
- T의 제로 값. 제네릭 코드에서 "아무것도 없음"을 반환하는 방법은
var zero T입니다. 모든 타입에 통하는T{}나nil은 없습니다. - 인스턴스화. 제네릭 타입은 쓰기 전에 인스턴스화해야 합니다.
Stack이 아니라Stack[string]입니다. 자기 메서드 안에서는 리시버를Stack[T]로 씁니다.
Go 1.24에는 제네릭 타입 별칭이 추가되어 type Set[T comparable] = map[T]struct{}가 이제 컴파일됩니다.
부딪히게 될 한계
- 메서드에 타입 매개변수가 없습니다.
func (s *Stack[T]) Map[U any](...)는 허용되지 않습니다. 최상위 함수를 쓰세요. - 특수화가 없습니다.
T = string일 때를 위한 별도 구현을 쓸 수 없습니다. 함수 안에서any(v)에 타입 스위치를 쓰는 것이 우회책이지만, 보통은 제네릭이 맞지 않는 도구라는 신호입니다. - 연산자를 이름으로 제약할 수 없습니다. "
+메서드가 있는 모든 타입"이라고 말할 수 없고, 유니온에 타입을 나열해야 합니다. - 제약을 통한 필드 접근은 안 됩니다. 유니온의 모든 타입에
ID필드가 있어도v.ID는 허용되지 않습니다. 대신 제약에 메서드를 넣으세요.
제네릭을 쓰지 말아야 할 때
제네릭은 컨테이너 타입(스택, 집합, 캐시, 트리)과, 요소 타입에 상관없이 같은 일을 하는 슬라이스와 맵 알고리즘에 어울립니다. 모든 곳에 어울리지는 않습니다.
- 구체 타입이 하나뿐일 때. 함수가 언제나
[]User로만 호출된다면 제네릭 버전은 읽는 비용만 늘립니다. - 타입에 따라 동작이 다를 때. 그것이 인터페이스의 역할입니다.
io.Writer를 받는 함수가[T io.Writer]를 받는 함수보다 단순하고, 동작도 같습니다. - 인터페이스 매개변수를 대체할 때.
func Print[T fmt.Stringer](v T)는func Print(v fmt.Stringer)보다 나을 게 없습니다.
Go 팀의 지침은 이렇습니다. 먼저 특정 타입에 대한 코드를 작성하고, 타입만 바꾼 같은 코드를 두 번째로 쓰고 있다는 걸 알아차렸을 때 타입 매개변수를 꺼내 드세요.
흔한 실수
comparable이나cmp.Ordered가 필요한 곳에any를 씀. 컴파일러는any타입 매개변수에 대한==나<를 거부합니다.~를 잊음.type ID int같은 사용자 정의 타입은 그냥int로 된 제약을 통과하지 못합니다.- 타입 매개변수에 대해
nil을 반환함. 제약이 T를 포인터 같은 타입으로 한정하지 않는 한 허용되지 않습니다. 제로 값을 반환하세요. - 이미 있는 제네릭 헬퍼를 직접 작성함.
slices,maps,cmp부터 확인하세요.
자주 묻는 질문
Go에 제네릭이 있나요?
네, Go 1.18(2022년 3월)부터 있습니다. 함수와 타입은 대괄호 안에 타입 매개변수를 선언할 수 있습니다: func Map[T, U any](s []T, f func(T) U) []U. 각 타입 매개변수에는 제약이 있고, 제약은 그 타입이 지원해야 하는 것을 설명하는 인터페이스입니다.
Go 제네릭에서 any와 comparable의 차이는 무엇인가요?
any는 모든 타입을 허용하지만 값으로 할 수 있는 일은 대입, 전달, 저장 정도뿐입니다. comparable은 ==와 !=를 지원하는 타입만 허용하며, 맵 키나 동등 비교에 필요한 것이 바로 이것입니다. <와 >가 필요하면 cmp.Ordered를 쓰세요.
Go 제약에서 물결표(~)는 무슨 뜻인가요?
~T는 "기반 타입이 T인 모든 타입"을 뜻합니다. ~int는 int뿐 아니라 type Celsius int와도 일치합니다. 물결표 없이 제약에 int라고 쓰면 int 자체와만 일치하므로 사용자 정의 타입은 거부됩니다.
Go 메서드에 타입 매개변수를 둘 수 있나요?
없습니다. 메서드는 리시버 타입의 타입 매개변수를 쓸 수는 있지만(func (s *Stack[T]) Push(v T)), 새 타입 매개변수를 선언할 수는 없습니다. 대신 최상위 제네릭 함수를 쓰세요: func Map[T, U any](s *Stack[T], f func(T) U) *Stack[U].