타입을 미리 정하지 않고 함수를 쓴다
— Go 1.18이 바꿔 놓은 것
타입을 언제 결정하는가 — 이 한 가지만 바뀐다. 그리고 Go가 왜 12년 동안 이 기능을 미뤘는지도 함께 본다.
// T는 아직 정해지지 않은 타입 func printAny[T any](a T) { fmt.Println(a) } // 호출하는 순간 결정된다 printAny(10) // T = int printAny("hello") // T = string
Go의 핵심 철학은 단순함. Rob Pike를 비롯한 설계자들은 언어 복잡도를 낮추려 많은 기능을 의도적으로 뺐다.
Generics도 오랫동안 그 대상이었다
커뮤니티에서 가장 많이 요청된 기능이었다. Ian Lance Taylor의 Type Parameters Proposal이 채택돼 1.18에 들어갔다.
interface{} 우회는 한계가 뚜렷했다
타입 안전성을 런타임까지 미루는 interface{},
그리고 타입 수만큼 늘어나는 복붙 함수. 이 둘이 도입의 실제 명분이다.
// interface{} 기반 — 리턴이 interface{}라 타입 단언이 필요하다 func foo1(a interface{}) interface{} { return a } // generics 기반 — 리턴이 T라 단언이 필요 없다 func foo2[T any](a T) T { return a } func main() { var a, b int = 10, 20 var c int c = foo1(a).(int) // 타입 단언 필요 — 런타임에 실패 가능 c = foo2(b) // 타입이 자동 결정 — 컴파일 타임 안전 }
.(int) 한 조각이 사라졌다. 그런데 사라진 건 문법이 아니라 런타임 panic 가능성이다.
interface{}에서 구체 타입을 꺼내는 연산value.(Type)ok로 안전하게 받을 수 있다var val interface{} = "hello" s := val.(string) // OK i := val.(int) // panic! // 안전한 형태 s, ok := val.(string) // true i, ok := val.(int) // false, 0
잘못된 단언은 컴파일이 아니라 런타임 panic으로 드러난다.
테스트가 안 짚으면 배포 후에 만난다
값을 쓸 때마다 .(Type)을 반복해서 붙여야 한다.
호출부가 지저분해진다
반환 타입이 interface{}라 자동완성·타입 체크가 막힌다.
에디터가 도와줄 수 없다
func minInt(a, b int) int { if a < b { return a } return b } func minInt16(a, b int16) int16 { if a < b { return a } // 로직이 완전히 동일 return b } func minFloat64(a, b float64) float64 { if a < b { return a } // 또 동일한 로직... return b }
타입 파라미터 선언부터 generic 함수 · constraint · generic 구조체까지.
앞에서 본 min 중복이 어떻게 한 개로 줄어드는지 따라간다.
any가 가장 느슨하다any는 모든 타입을 허용하는 constraint< + 같은 연산은 쓸 수 없다// any = 모든 타입 허용 func printAny[T any](a T) { fmt.Println(a) } func main() { printAny(10) // T = int printAny(3.14) // T = float64 printAny("hello") // T = string }
// 허용할 타입을 | 로 나열한다 (union type constraint) func minType[T int | int16 | int32 | int64 | float32 | float64](a, b T) T { if a < b { return a } return b } func main() { fmt.Println(minType(10, 20)) // int: 10 fmt.Println(minType(int16(10), int16(20))) // int16: 10 fmt.Println(minType(3.14, 1.14)) // float64: 1.14 }
any는 < 연산을 보장하지 않는다. 비교하려면 비교 가능한 타입만 받아야 한다.
interface 키워드로 이름 붙인 constraint를 만든다type IntegerType interface { int | int16 | int32 | int64 } type Float interface { float32 | float64 } // 합성도 가능하다 type ComparableNumbers interface { IntegerType | Float }
func minComparableNumbers[T ComparableNumbers](a, b T) T
type Node[T any] struct { val T next *Node[T] // 자기 자신도 T로 참조한다 } func NewNode[T any](v T) *Node[T] { return &Node[T]{val: v} } // 사용 node := NewNode(1) // *Node[int] node.Push(2).Push(3).Push(4) strNode := NewNode("hello") // *Node[string] strNode.Push("world")
IntNode · StringNode를 따로 정의할 필요가 없다. 자료구조 중복이 사라진다.
func (n *Node[T]) Push(v T) *Node[T]
구조체에 선언된 타입 파라미터는 메서드에서 자유롭게 쓴다
func (n *Node[T]) Push[F any](f F)
컴파일 에러. Go는 이 문법을 허용하지 않는다
// F: 입력 요소 타입, T: 출력 요소 타입 func Map[F, T any](s []F, f func(F) T) []T { rst := make([]T, len(s)) for i, v := range s { rst[i] = f(v) } return rst } doubled := Map([]int{1, 2, 3}, func(i int) int { return i * 2 }) // [2 4 6] uppered := Map([]string{"Hello", "world"}, strings.ToUpper) // [HELLO WORLD]
F와 T가 달라도 되는 것이 요점이다. 그래야 []int → []string 변환도 같은 함수로 쓴다.
지금까지 호출부에 [int]를 거의 안 썼다.
컴파일러가 인자에서 타입을 알아내기 때문이다 — 언제 되고 언제 안 되는지 본다.
func identity[T any](v T) T { return v } // 명시적 타입 지정 r1 := identity[int](42) r2 := identity[string]("hello") // 타입 추론 — 인자로부터 자동 r3 := identity(42) // T = int r4 := identity("hello") // T = string
func pair[T, U any](a T, b U) string { return fmt.Sprintf("(%v, %v)", a, b) } // 모두 추론 가능 pair(1, "hello") // T=int, U=string pair(3.14, true) // T=float64, U=bool
pair[int, string](...)처럼 전부 적는다.
func toSlice[T any](args ...T) []T { return args } // 추론 성공 — 인자에서 타입 결정 ints := toSlice(1, 2, 3) // T = int // 추론 실패 — 인자가 없어 결정 불가, 명시 필요 emptyInts := toSlice[int]() emptyStrings := toSlice[string]() result := toSlice() // 컴파일 에러: cannot infer T
| 항목 | Generics 도입 전 | Generics 도입 후 |
|---|---|---|
| 여러 타입 지원 | interface{} + 타입 단언 | 타입 파라미터 [T any] |
| 타입 안전성 | 런타임 panic 위험 | 컴파일 타임 검증 |
| 코드 중복 | 타입별 함수 반복 작성 | 하나의 Generic 함수 |
| 자료구조 | 타입별 struct 정의 | Generic struct [T any] |
| IDE 지원 | 제한적 | 완전한 타입 추론 |
T이므로 컴파일 타임에 타입이 확정된다. → 슬라이드 06 · 08any는 < 연산을 보장하지 않는다. 비교하려면 union type 등으로 비교 가능한 타입만 받도록 좁혀야 한다. → 슬라이드 12 · 13toSlice[int]()처럼 명시해야 한다. → 슬라이드 21pair[int, string](...)처럼 전부 적는다. → 슬라이드 20대괄호로 타입 자리를 비워두고, constraint로 그 자리를 얼마나 열어둘지 정한다.
호출부는 추론 덕분에 거의 그대로다
any · comparable · union(|) · tilde(~) · 커스텀 constraint 설계.
오늘 남겨둔 질문들이 여기서 풀린다