Menu

Golang 제네릭: 타입 매개변수, 제약, 예제

제네릭(Go 1.18 이상)을 쓰면 함수나 타입 하나가 타입 안전성을 유지하면서 여러 타입에 동작합니다. 타입 매개변수, any, comparable, cmp.Ordered 제약, ~를 쓴 커스텀 제약, 제네릭 타입, 그리고 쓰지 말아야 할 때를 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

제네릭 함수

타입 매개변수는 일반 매개변수 앞의 대괄호 안에 씁니다. 각 타입 매개변수에는 허용되는 타입을 정하는 제약이 있습니다.

TU는 인자로부터 추론되므로 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.Indexslices.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인 모든 타입"을 뜻합니다. ~intint뿐 아니라 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].

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기