타입 파라미터 선언
타입 파라미터 선언
제네릭을 처음 만나면 '타입 파라미터'라는 단어가 낯설게 느껴지는데요, 사실 별것 아니에요. 여러 타입에 공통으로 동작하는 코드를 한 번만 쓰고 싶을 때, 그 '아직 정해지지 않은 타입'의 자리를 마련해 두는 게 바로 타입 파라미터랍니다. Go에서는 함수나 타입을 선언할 때 이 타입 파라미터를 대괄호 [ ] 안에 나열해요. 이번 섹션에서 타입 파라미터가 어떻게 선언되고, 어떤 제약을 받는지 차근차근 살펴볼게요.
출처: Go Specification
본문
타입 파라미터 리스트(type parameter list)는 제네릭 함수(generic function), 메서드(method), 또는 타입 선언(type declaration)의 타입 파라미터들을 선언합니다. 이 타입 파라미터 리스트는 일반적인 함수 파라미터 리스트와 생김새는 비슷한데요, 두 가지만 다릅니다. 첫째, 타입 파라미터의 이름이 전부 있어야 하고, 둘째, 소괄호가 아니라 대괄호로 감싼다는 점이에요 [Go 1.18, Go 1.27].
TypeParameters = "[" TypeParamList [ "," ] "]" .
TypeParamList = TypeParamDecl { "," TypeParamDecl } .
TypeParamDecl = IdentifierList TypeConstraint .
리스트에 있는 모든 비-블랭크(non-blank) 이름은 서로 달라야 해요. 각 이름은 하나의 타입 파라미터를 선언하는데, 이 타입 파라미터는 선언 내부에서 아직 알려지지 않은 타입을 대신하는 '자리표시자' 역할을 하는 **새로운 명명된 타입(named type)**이에요. 제네릭 함수, 메서드, 또는 타입을 **인스턴스화(instantiation)**할 때 이 타입 파라미터가 **타입 인자(type argument)**로 대체됩니다.
[P any]
[S interface{ ~[]byte|string }]
[S ~[]E, E any]
[P Constraint[int]]
[_ any]
일반적인 함수 파라미터가 각자 '파라미터 타입'을 가지듯이, 각 타입 파라미터도 대응하는 (메타)타입을 가지는데요, 이걸 **타입 제약(type constraint)**이라고 불러요.
여기서 재미있는 모호성 문제가 하나 생겨요. 제네릭 타입 선언에서 타입 파라미터 P와 그 제약 C를 하나만 선언했는데, 그 텍스트 P C가 그 자체로 유효한 표현식이 되는 경우를 생각해 볼게요:
type T[P *C] …
type T[P (C)] …
type T[P *C|Q] …
…
이런 드문 경우에는 타입 파라미터 리스트가 표현식과 구분되지 않아서, 그 타입 선언은 배열 타입 선언(array type declaration)으로 해석돼버려요. 이 모호성을 해결하려면 제약을 인터페이스 안에 넣거나, 끝에 콤마를 붙여 주시면 돼요:
type T[P interface{*C}] …
type T[P *C,] …
타입 파라미터는 또한 제네릭 타입에 연관된 **메서드 선언(method declaration)**의 리시버 명세(receiver specification)를 통해서도 선언될 수 있어요.
타입 제약
**타입 제약(type constraint)**은 해당 타입 파라미터에 허용되는 타입 인자들의 집합을 정의하고, 그 타입 파라미터 타입의 값들이 지원하는 연산을 제어하는 인터페이스입니다 [Go 1.18].
TypeConstraint = TypeElem .
만약 제약이 interface{E} 형태의 인터페이스 리터럴인 경우를 볼게요. 여기서 E는 (메서드가 아닌) 임베디드 타입 요소(type element)인데요, 타입 파라미터 리스트 안에서는 편의를 위해 바깥의 interface{ … }를 생략할 수 있어요:
[T []P] // = [T interface{[]P}]
[T ~int] // = [T interface{~int}]
[T int|string] // = [T interface{int|string}]
type Constraint ~int // illegal: ~int is not in a type parameter list
미리 선언된(predeclared) 인터페이스 타입 **comparable**은 **엄격히 비교 가능(strictly comparable)**한 모든 비-인터페이스 타입의 집합을 나타냅니다 [Go 1.18].
여기서 한 가지 꼭 짚고 넘어갈 점이 있어요. 타입 파라미터가 아닌 인터페이스들은 비교 가능(comparable)하긴 하지만, 엄격히 비교 가능하지는 않아서 comparable을 구현(implement)하지 못해요. 그렇지만 comparable을 충족(satisfy)하기는 합니다. 이 두 개념의 차이에 주의하세요.
int // implements comparable (int is strictly comparable)
[]byte // does not implement comparable (slices cannot be compared)
interface{} // does not implement comparable (see above)
interface{ ~int | ~string } // type parameter only: implements comparable (int, string types are strictly comparable)
interface{ comparable } // type parameter only: implements comparable (comparable implements itself)
interface{ ~int | ~[]byte } // type parameter only: does not implement comparable (slices are not comparable)
interface{ ~struct{ any } } // type parameter only: does not implement comparable (field any is not strictly comparable)
comparable 인터페이스와, (직접적이든 간접적이든) comparable을 임베드하는 인터페이스들은 타입 제약으로만 사용할 수 있어요. 이들은 값이나 변수의 타입이 될 수도 없고, 다른 비-인터페이스 타입의 구성 요소가 될 수도 없습니다.
타입 제약 충족
타입 인자 T가 타입 제약 C를 **충족(satisfies)**한다는 것은, T가 C가 정의하는 타입 집합(type set)의 원소라는 뜻이에요. 다시 말해 T가 C를 **구현(implements)**한다는 의미죠. 다만 예외가 하나 있어요. 엄격히 비교 가능한 타입 제약은 비교 가능한(반드시 엄격히 비교 가능할 필요는 없는) 타입 인자로도 충족될 수 있어요 [Go 1.20]. 좀 더 정확히 말하면 다음과 같아요.
타입 T가 제약 C를 충족한다는 것은 다음 둘 중 하나가 성립할 때예요:
T가C를 구현(implements)한다. 또는C가interface{ comparable; E }꼴로 쓸 수 있고, 여기서E는 기본 인터페이스(basic interface)이며T는 비교 가능하면서E를 구현한다.
type argument type constraint // constraint satisfaction
int interface{ ~int } // satisfied: int implements interface{ ~int }
string comparable // satisfied: string implements comparable (string is strictly comparable)
[]byte comparable // not satisfied: slices are not comparable
any interface{ comparable; int } // not satisfied: any does not implement interface{ int }
any comparable // satisfied: any is comparable and implements the basic interface any
struct{f any} comparable // satisfied: struct{f any} is comparable and implements the basic interface any
any interface{ comparable; m() } // not satisfied: any does not implement the basic interface interface{ m() }
interface{ m() } interface{ comparable; m() } // satisfied: interface{ m() } is comparable and implements the basic interface interface{ m() }
제약 충족 규칙에 이 예외가 있기 때문에, 타입 파라미터 타입의 피연산자를 비교하면 런타임에 패닉(panic)이 발생할 수 있어요. 비교 가능한 타입 파라미터가 항상 엄격히 비교 가능하다 하더라도, 그렇기 때문에 생기는 일이니 꼭 기억해 두세요.
더 알아보기
- Type declarations — 타입 선언에서 타입 파라미터가 어떻게 쓰이는지
- Function declarations — 제네릭 함수 선언에서의 타입 파라미터
- Interface types — 타입 제약의 바탕이 되는 인터페이스
- Instantiations — 타입 파라미터가 타입 인자로 대체되는 과정
- Comparison operators — 비교 가능(comparable)과 엄격히 비교 가능(strictly comparable)의 차이