Updated:

20 minute read

개요

Go 1.18부터 제네릭(Type Parameters)이 추가되어 타입 안전하고 재사용 가능한 코드 작성이 가능해졌습니다.

주요 특징:

  • 타입 파라미터: 함수와 타입에 타입 매개변수 사용
  • 타입 제약: 인터페이스 기반 타입 제약 정의
  • 타입 추론: 컴파일러가 타입 자동 추론
  • 타입 셋: Union 타입과 approximate 타입
  • 컴파일 타임 안전성: 런타임 오버헤드 없음
  • 표준 라이브러리: constraints, slices, maps 패키지
  • 성능: 인터페이스보다 빠른 경우 많음

기본 개념

1. 제네릭이 없는 경우의 문제

// 제네릭 이전: 타입별로 중복 코드
func MinInt(a, b int) int {
    if a < b {
        return a
    }
    return b
}

func MinFloat64(a, b float64) float64 {
    if a < b {
        return a
    }
    return b
}

func MinString(a, b string) string {
    if a < b {
        return a
    }
    return b
}

// 또는 interface{}를 사용하지만 타입 안전성 상실
func MinInterface(a, b interface{}) interface{} {
    // 타입 체크 필요, 런타임 오류 가능
    // ...
}

2. 제네릭 함수

package main

import (
    "fmt"
    "golang.org/x/exp/constraints"
)

// 제네릭 함수: 타입 파라미터 T
func Min[T constraints.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func main() {
    // 타입 명시
    fmt.Println(Min[int](10, 20))           // 10
    fmt.Println(Min[float64](3.14, 2.71))   // 2.71
    fmt.Println(Min[string]("hello", "world")) // hello
    
    // 타입 추론 (권장)
    fmt.Println(Min(10, 20))          // 10
    fmt.Println(Min(3.14, 2.71))      // 2.71
    fmt.Println(Min("hello", "world")) // hello
}

3. 제네릭 타입

// 제네릭 구조체
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, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

func (s *Stack[T]) Peek() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    return s.items[len(s.items)-1], true
}

func (s *Stack[T]) Size() int {
    return len(s.items)
}

func main() {
    // int 스택
    intStack := &Stack[int]{}
    intStack.Push(1)
    intStack.Push(2)
    intStack.Push(3)
    
    fmt.Println(intStack.Pop()) // 3, true
    fmt.Println(intStack.Peek()) // 2, true
    
    // string 스택
    strStack := &Stack[string]{}
    strStack.Push("hello")
    strStack.Push("world")
    
    fmt.Println(strStack.Pop()) // world, true
    fmt.Println(strStack.Size()) // 1
}

타입 제약 (Type Constraints)

1. any 제약

// any는 interface{}의 별칭 (모든 타입 허용)
func Print[T any](value T) {
    fmt.Printf("Value: %v, Type: %T\n", value, value)
}

func main() {
    Print(42)           // Value: 42, Type: int
    Print("hello")      // Value: hello, Type: string
    Print(3.14)         // Value: 3.14, Type: float64
    Print([]int{1, 2})  // Value: [1 2], Type: []int
}

2. comparable 제약

// comparable: ==, != 연산 가능한 타입
func Contains[T comparable](slice []T, target T) bool {
    for _, item := range slice {
        if item == target {
            return true
        }
    }
    return false
}

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    fmt.Println(Contains(numbers, 3))    // true
    fmt.Println(Contains(numbers, 10))   // false
    
    words := []string{"go", "rust", "python"}
    fmt.Println(Contains(words, "go"))   // true
    
    // 슬라이스는 comparable이 아니므로 컴파일 에러
    // slices := [][]int{{1}, {2}}
    // Contains(slices, []int{1}) // 에러!
}

3. 인라인 제약

// Union 타입: | 사용
func Add[T int | float64 | string](a, b T) T {
    return a + b
}

func main() {
    fmt.Println(Add(1, 2))          // 3
    fmt.Println(Add(1.5, 2.5))      // 4.0
    fmt.Println(Add("hello", " world")) // hello world
    
    // bool은 제약에 없으므로 컴파일 에러
    // Add(true, false) // 에러!
}

4. 인터페이스 제약

// 제약 인터페이스 정의
type Number interface {
    int | int64 | float64
}

func Sum[T Number](numbers []T) T {
    var sum T
    for _, n := range numbers {
        sum += n
    }
    return sum
}

func main() {
    ints := []int{1, 2, 3, 4, 5}
    fmt.Println(Sum(ints)) // 15
    
    floats := []float64{1.1, 2.2, 3.3}
    fmt.Println(Sum(floats)) // 6.6
}

5. 타입 근사 (Type Approximation)

// ~ 사용: 기본 타입이 같은 사용자 정의 타입 포함
type Integer interface {
    ~int | ~int64
}

type MyInt int

func Double[T Integer](n T) T {
    return n * 2
}

func main() {
    var x int = 5
    fmt.Println(Double(x)) // 10
    
    var y MyInt = 5
    fmt.Println(Double(y)) // 10 (~ 덕분에 가능)
}

6. 메서드 제약

type Stringer interface {
    String() string
}

func PrintAll[T Stringer](items []T) {
    for _, item := range items {
        fmt.Println(item.String())
    }
}

type Person struct {
    Name string
    Age  int
}

func (p Person) String() string {
    return fmt.Sprintf("%s (%d)", p.Name, p.Age)
}

func main() {
    people := []Person{
        {"Alice", 30},
        {"Bob", 25},
    }
    PrintAll(people)
    // Alice (30)
    // Bob (25)
}

7. 복합 제약

// 메서드 + 타입 제약
type Numeric interface {
    ~int | ~int64 | ~float64
    String() string
}

// 여러 제약 조합
type ComparableNumber interface {
    comparable
    ~int | ~int64 | ~float64
}

func FindMax[T ComparableNumber](numbers []T) (T, bool) {
    if len(numbers) == 0 {
        var zero T
        return zero, false
    }
    
    max := numbers[0]
    for _, n := range numbers[1:] {
        if n > max {
            max = n
        }
    }
    return max, true
}

표준 라이브러리 제약

1. constraints 패키지

import "golang.org/x/exp/constraints"

// Ordered: <, <=, >=, > 연산 가능
func Max[T constraints.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

// Signed: 부호 있는 정수
func Abs[T constraints.Signed](n T) T {
    if n < 0 {
        return -n
    }
    return n
}

// Unsigned: 부호 없는 정수
func IsEven[T constraints.Unsigned](n T) bool {
    return n%2 == 0
}

// Integer: 모든 정수 타입
func Factorial[T constraints.Integer](n T) T {
    if n <= 1 {
        return 1
    }
    return n * Factorial(n-1)
}

// Float: 모든 부동소수점 타입
func Round[T constraints.Float](n T) T {
    return T(int(n + 0.5))
}

func main() {
    fmt.Println(Max(10, 20))        // 20
    fmt.Println(Max("a", "z"))      // z
    
    fmt.Println(Abs(-42))           // 42
    fmt.Println(Abs(-3.14))         // 3.14
    
    fmt.Println(IsEven(uint(4)))    // true
    fmt.Println(IsEven(uint(5)))    // false
    
    fmt.Println(Factorial(5))       // 120
    
    fmt.Println(Round(3.14))        // 3
    fmt.Println(Round(3.7))         // 4
}

2. 제약 정의

// constraints 패키지의 제약들
type Signed interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

type Unsigned interface {
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}

type Integer interface {
    Signed | Unsigned
}

type Float interface {
    ~float32 | ~float64
}

type Complex interface {
    ~complex64 | ~complex128
}

type Ordered interface {
    Integer | Float | ~string
}

제네릭 데이터 구조

1. 제네릭 슬라이스

type Slice[T any] []T

func (s Slice[T]) Map(f func(T) T) Slice[T] {
    result := make(Slice[T], len(s))
    for i, v := range s {
        result[i] = f(v)
    }
    return result
}

func (s Slice[T]) Filter(f func(T) bool) Slice[T] {
    result := Slice[T]{}
    for _, v := range s {
        if f(v) {
            result = append(result, v)
        }
    }
    return result
}

func (s Slice[T]) Reduce(f func(T, T) T, initial T) T {
    result := initial
    for _, v := range s {
        result = f(result, v)
    }
    return result
}

func main() {
    numbers := Slice[int]{1, 2, 3, 4, 5}
    
    // Map: 각 요소를 2배로
    doubled := numbers.Map(func(n int) int { return n * 2 })
    fmt.Println(doubled) // [2 4 6 8 10]
    
    // Filter: 짝수만
    evens := numbers.Filter(func(n int) bool { return n%2 == 0 })
    fmt.Println(evens) // [2 4]
    
    // Reduce: 합계
    sum := numbers.Reduce(func(a, b int) int { return a + b }, 0)
    fmt.Println(sum) // 15
}

2. 제네릭 맵

type Map[K comparable, V any] map[K]V

func (m Map[K, V]) Keys() []K {
    keys := make([]K, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    return keys
}

func (m Map[K, V]) Values() []V {
    values := make([]V, 0, len(m))
    for _, v := range m {
        values = append(values, v)
    }
    return values
}

func (m Map[K, V]) Filter(f func(K, V) bool) Map[K, V] {
    result := make(Map[K, V])
    for k, v := range m {
        if f(k, v) {
            result[k] = v
        }
    }
    return result
}

func main() {
    ages := Map[string, int]{
        "Alice": 30,
        "Bob":   25,
        "Carol": 35,
    }
    
    fmt.Println(ages.Keys())   // [Alice Bob Carol]
    fmt.Println(ages.Values()) // [30 25 35]
    
    // 30세 이상만
    over30 := ages.Filter(func(name string, age int) bool {
        return age >= 30
    })
    fmt.Println(over30) // map[Alice:30 Carol:35]
}

3. 제네릭 링크드 리스트

type Node[T any] struct {
    Value T
    Next  *Node[T]
}

type LinkedList[T any] struct {
    head *Node[T]
    tail *Node[T]
    size int
}

func (l *LinkedList[T]) Append(value T) {
    node := &Node[T]{Value: value}
    
    if l.head == nil {
        l.head = node
        l.tail = node
    } else {
        l.tail.Next = node
        l.tail = node
    }
    l.size++
}

func (l *LinkedList[T]) Prepend(value T) {
    node := &Node[T]{Value: value, Next: l.head}
    l.head = node
    
    if l.tail == nil {
        l.tail = node
    }
    l.size++
}

func (l *LinkedList[T]) ToSlice() []T {
    result := make([]T, 0, l.size)
    current := l.head
    
    for current != nil {
        result = append(result, current.Value)
        current = current.Next
    }
    
    return result
}

func (l *LinkedList[T]) Size() int {
    return l.size
}

func main() {
    list := &LinkedList[string]{}
    list.Append("World")
    list.Prepend("Hello")
    list.Append("!")
    
    fmt.Println(list.ToSlice()) // [Hello World !]
    fmt.Println(list.Size())    // 3
}

4. 제네릭 트리

type TreeNode[T any] struct {
    Value T
    Left  *TreeNode[T]
    Right *TreeNode[T]
}

type BinaryTree[T constraints.Ordered] struct {
    root *TreeNode[T]
}

func (bt *BinaryTree[T]) Insert(value T) {
    bt.root = bt.insertNode(bt.root, value)
}

func (bt *BinaryTree[T]) insertNode(node *TreeNode[T], value T) *TreeNode[T] {
    if node == nil {
        return &TreeNode[T]{Value: value}
    }
    
    if value < node.Value {
        node.Left = bt.insertNode(node.Left, value)
    } else if value > node.Value {
        node.Right = bt.insertNode(node.Right, value)
    }
    
    return node
}

func (bt *BinaryTree[T]) InOrder() []T {
    result := []T{}
    bt.inOrder(bt.root, &result)
    return result
}

func (bt *BinaryTree[T]) inOrder(node *TreeNode[T], result *[]T) {
    if node == nil {
        return
    }
    
    bt.inOrder(node.Left, result)
    *result = append(*result, node.Value)
    bt.inOrder(node.Right, result)
}

func main() {
    tree := &BinaryTree[int]{}
    tree.Insert(5)
    tree.Insert(3)
    tree.Insert(7)
    tree.Insert(1)
    tree.Insert(9)
    
    fmt.Println(tree.InOrder()) // [1 3 5 7 9]
}

5. 제네릭 큐

type Queue[T any] struct {
    items []T
}

func (q *Queue[T]) Enqueue(item T) {
    q.items = append(q.items, item)
}

func (q *Queue[T]) Dequeue() (T, bool) {
    if len(q.items) == 0 {
        var zero T
        return zero, false
    }
    
    item := q.items[0]
    q.items = q.items[1:]
    return item, true
}

func (q *Queue[T]) Peek() (T, bool) {
    if len(q.items) == 0 {
        var zero T
        return zero, false
    }
    return q.items[0], true
}

func (q *Queue[T]) Size() int {
    return len(q.items)
}

func (q *Queue[T]) IsEmpty() bool {
    return len(q.items) == 0
}

func main() {
    queue := &Queue[string]{}
    
    queue.Enqueue("first")
    queue.Enqueue("second")
    queue.Enqueue("third")
    
    fmt.Println(queue.Dequeue()) // first, true
    fmt.Println(queue.Peek())    // second, true
    fmt.Println(queue.Size())    // 2
}

다중 타입 파라미터

1. 두 개의 타입 파라미터

type Pair[T, U any] struct {
    First  T
    Second U
}

func NewPair[T, U any](first T, second U) Pair[T, U] {
    return Pair[T, U]{First: first, Second: second}
}

func (p Pair[T, U]) Swap() Pair[U, T] {
    return Pair[U, T]{First: p.Second, Second: p.First}
}

func main() {
    // string과 int 페어
    p1 := NewPair("age", 30)
    fmt.Printf("%+v\n", p1) // {First:age Second:30}
    
    // Swap
    p2 := p1.Swap()
    fmt.Printf("%+v\n", p2) // {First:30 Second:age}
}

2. 맵 변환

func MapKeys[K comparable, V any, R any](
    m map[K]V,
    f func(K) R,
) []R {
    result := make([]R, 0, len(m))
    for k := range m {
        result = append(result, f(k))
    }
    return result
}

func MapValues[K comparable, V any, R any](
    m map[K]V,
    f func(V) R,
) []R {
    result := make([]R, 0, len(m))
    for _, v := range m {
        result = append(result, f(v))
    }
    return result
}

func main() {
    ages := map[string]int{
        "Alice": 30,
        "Bob":   25,
    }
    
    // 키를 대문자로
    upperKeys := MapKeys(ages, func(k string) string {
        return strings.ToUpper(k)
    })
    fmt.Println(upperKeys) // [ALICE BOB]
    
    // 값을 문자열로
    ageStrings := MapValues(ages, func(v int) string {
        return fmt.Sprintf("%d years", v)
    })
    fmt.Println(ageStrings) // [30 years 25 years]
}

3. Result 타입

type Result[T any, E error] struct {
    value T
    err   E
}

func Ok[T any, E error](value T) Result[T, E] {
    return Result[T, E]{value: value}
}

func Err[T any, E error](err E) Result[T, E] {
    return Result[T, E]{err: err}
}

func (r Result[T, E]) Unwrap() (T, E) {
    return r.value, r.err
}

func (r Result[T, E]) IsOk() bool {
    return r.err == nil
}

func (r Result[T, E]) IsErr() bool {
    return r.err != nil
}

func Divide(a, b float64) Result[float64, error] {
    if b == 0 {
        return Err[float64, error](fmt.Errorf("division by zero"))
    }
    return Ok[float64, error](a / b)
}

func main() {
    result1 := Divide(10, 2)
    if result1.IsOk() {
        value, _ := result1.Unwrap()
        fmt.Println("Result:", value) // Result: 5
    }
    
    result2 := Divide(10, 0)
    if result2.IsErr() {
        _, err := result2.Unwrap()
        fmt.Println("Error:", err) // Error: division by zero
    }
}

4. Either 타입

type Either[L, R any] struct {
    left  *L
    right *R
}

func Left[L, R any](value L) Either[L, R] {
    return Either[L, R]{left: &value}
}

func Right[L, R any](value R) Either[L, R] {
    return Either[L, R]{right: &value}
}

func (e Either[L, R]) IsLeft() bool {
    return e.left != nil
}

func (e Either[L, R]) IsRight() bool {
    return e.right != nil
}

func (e Either[L, R]) GetLeft() (L, bool) {
    if e.left != nil {
        return *e.left, true
    }
    var zero L
    return zero, false
}

func (e Either[L, R]) GetRight() (R, bool) {
    if e.right != nil {
        return *e.right, true
    }
    var zero R
    return zero, false
}

func ParseInt(s string) Either[error, int] {
    value, err := strconv.Atoi(s)
    if err != nil {
        return Left[error, int](err)
    }
    return Right[error, int](value)
}

func main() {
    result1 := ParseInt("123")
    if value, ok := result1.GetRight(); ok {
        fmt.Println("Parsed:", value) // Parsed: 123
    }
    
    result2 := ParseInt("abc")
    if err, ok := result2.GetLeft(); ok {
        fmt.Println("Error:", err) // Error: ...
    }
}

타입 추론

1. 함수 인자 추론

func Identity[T any](value T) T {
    return value
}

func main() {
    // 타입 명시
    x := Identity[int](42)
    
    // 타입 추론 (권장)
    y := Identity(42)        // T는 int로 추론
    z := Identity("hello")   // T는 string으로 추론
    
    fmt.Println(x, y, z) // 42 42 hello
}

2. 타입 추론 제한

func MakePair[T any](first, second T) Pair[T, T] {
    return Pair[T, T]{First: first, Second: second}
}

func main() {
    // 같은 타입이어야 함
    p1 := MakePair(1, 2)         // OK: 둘 다 int
    p2 := MakePair("a", "b")     // OK: 둘 다 string
    
    // 다른 타입이면 컴파일 에러
    // p3 := MakePair(1, "a")    // 에러!
    
    // 명시적으로 타입 지정 가능
    p4 := MakePair[any](1, "a") // OK: any로 변환
}

3. 반환 타입 추론

func Zero[T any]() T {
    var zero T
    return zero
}

func main() {
    // 컨텍스트에서 타입 추론
    var x int = Zero[int]()      // 명시
    var y string = Zero[string]() // 명시
    
    // 추론 불가능한 경우 명시 필요
    z := Zero[float64]() // 반드시 타입 지정
    
    fmt.Println(x, y, z) // 0  0
}

4. 슬라이스 타입 추론

func Map[T, U any](slice []T, f func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = f(v)
    }
    return result
}

func main() {
    numbers := []int{1, 2, 3}
    
    // T는 int, U는 string으로 추론
    strings := Map(numbers, func(n int) string {
        return fmt.Sprintf("%d", n)
    })
    
    fmt.Println(strings) // [1 2 3]
}

실전 예제

1. 제네릭 캐시

import (
    "sync"
    "time"
)

type CacheItem[T any] struct {
    Value      T
    Expiration time.Time
}

type Cache[K comparable, V any] struct {
    items map[K]CacheItem[V]
    mu    sync.RWMutex
    ttl   time.Duration
}

func NewCache[K comparable, V any](ttl time.Duration) *Cache[K, V] {
    return &Cache[K, V]{
        items: make(map[K]CacheItem[V]),
        ttl:   ttl,
    }
}

func (c *Cache[K, V]) Set(key K, value V) {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    c.items[key] = CacheItem[V]{
        Value:      value,
        Expiration: time.Now().Add(c.ttl),
    }
}

func (c *Cache[K, V]) Get(key K) (V, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    
    item, found := c.items[key]
    if !found {
        var zero V
        return zero, false
    }
    
    if time.Now().After(item.Expiration) {
        var zero V
        return zero, false
    }
    
    return item.Value, true
}

func (c *Cache[K, V]) Delete(key K) {
    c.mu.Lock()
    defer c.mu.Unlock()
    delete(c.items, key)
}

func (c *Cache[K, V]) Clear() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.items = make(map[K]CacheItem[V])
}

func main() {
    cache := NewCache[string, int](5 * time.Second)
    
    cache.Set("key1", 100)
    cache.Set("key2", 200)
    
    if value, found := cache.Get("key1"); found {
        fmt.Println("Found:", value) // Found: 100
    }
    
    time.Sleep(6 * time.Second)
    
    if _, found := cache.Get("key1"); !found {
        fmt.Println("Expired")
    }
}

2. 제네릭 옵셔널

type Optional[T any] struct {
    value   *T
    present bool
}

func Some[T any](value T) Optional[T] {
    return Optional[T]{value: &value, present: true}
}

func None[T any]() Optional[T] {
    return Optional[T]{present: false}
}

func (o Optional[T]) IsPresent() bool {
    return o.present
}

func (o Optional[T]) Get() T {
    if !o.present {
        panic("called Get on None")
    }
    return *o.value
}

func (o Optional[T]) GetOrElse(defaultValue T) T {
    if o.present {
        return *o.value
    }
    return defaultValue
}

func (o Optional[T]) Map(f func(T) T) Optional[T] {
    if !o.present {
        return None[T]()
    }
    return Some(f(*o.value))
}

func (o Optional[T]) Filter(f func(T) bool) Optional[T] {
    if !o.present || !f(*o.value) {
        return None[T]()
    }
    return o
}

func FindUser(id int) Optional[string] {
    users := map[int]string{
        1: "Alice",
        2: "Bob",
    }
    
    if name, found := users[id]; found {
        return Some(name)
    }
    return None[string]()
}

func main() {
    user1 := FindUser(1)
    if user1.IsPresent() {
        fmt.Println("User:", user1.Get()) // User: Alice
    }
    
    user2 := FindUser(99)
    fmt.Println("Default:", user2.GetOrElse("Guest")) // Default: Guest
    
    // Map
    upper := user1.Map(func(s string) string {
        return strings.ToUpper(s)
    })
    fmt.Println(upper.Get()) // ALICE
}

3. 제네릭 Reducer

func Reduce[T, U any](slice []T, initial U, f func(U, T) U) U {
    result := initial
    for _, v := range slice {
        result = f(result, v)
    }
    return result
}

func main() {
    numbers := []int{1, 2, 3, 4, 5}
    
    // 합계
    sum := Reduce(numbers, 0, func(acc, n int) int {
        return acc + n
    })
    fmt.Println("Sum:", sum) // Sum: 15
    
    // 곱
    product := Reduce(numbers, 1, func(acc, n int) int {
        return acc * n
    })
    fmt.Println("Product:", product) // Product: 120
    
    // 문자열 연결
    concat := Reduce(numbers, "", func(acc string, n int) string {
        return acc + fmt.Sprintf("%d", n)
    })
    fmt.Println("Concat:", concat) // Concat: 12345
    
    // 최대값
    max := Reduce(numbers, numbers[0], func(acc, n int) int {
        if n > acc {
            return n
        }
        return acc
    })
    fmt.Println("Max:", max) // Max: 5
}

4. 제네릭 유효성 검증

type Validator[T any] func(T) error

type Validation[T any] struct {
    validators []Validator[T]
}

func NewValidation[T any]() *Validation[T] {
    return &Validation[T]{}
}

func (v *Validation[T]) Add(validator Validator[T]) *Validation[T] {
    v.validators = append(v.validators, validator)
    return v
}

func (v *Validation[T]) Validate(value T) []error {
    var errors []error
    for _, validator := range v.validators {
        if err := validator(value); err != nil {
            errors = append(errors, err)
        }
    }
    return errors
}

// User 타입
type User struct {
    Name  string
    Email string
    Age   int
}

func main() {
    validation := NewValidation[User]().
        Add(func(u User) error {
            if u.Name == "" {
                return fmt.Errorf("name is required")
            }
            return nil
        }).
        Add(func(u User) error {
            if !strings.Contains(u.Email, "@") {
                return fmt.Errorf("invalid email")
            }
            return nil
        }).
        Add(func(u User) error {
            if u.Age < 0 || u.Age > 150 {
                return fmt.Errorf("invalid age")
            }
            return nil
        })
    
    // 유효한 사용자
    user1 := User{Name: "Alice", Email: "alice@example.com", Age: 30}
    errors1 := validation.Validate(user1)
    fmt.Println("Errors:", errors1) // Errors: []
    
    // 유효하지 않은 사용자
    user2 := User{Name: "", Email: "invalid", Age: -5}
    errors2 := validation.Validate(user2)
    for _, err := range errors2 {
        fmt.Println("-", err)
    }
    // - name is required
    // - invalid email
    // - invalid age
}

5. 제네릭 Builder 패턴

type Builder[T any] struct {
    value T
}

func NewBuilder[T any]() *Builder[T] {
    return &Builder[T]{}
}

func (b *Builder[T]) With(f func(*T)) *Builder[T] {
    f(&b.value)
    return b
}

func (b *Builder[T]) Build() T {
    return b.value
}

type Config struct {
    Host    string
    Port    int
    Timeout time.Duration
    Debug   bool
}

func main() {
    config := NewBuilder[Config]().
        With(func(c *Config) { c.Host = "localhost" }).
        With(func(c *Config) { c.Port = 8080 }).
        With(func(c *Config) { c.Timeout = 30 * time.Second }).
        With(func(c *Config) { c.Debug = true }).
        Build()
    
    fmt.Printf("%+v\n", config)
    // {Host:localhost Port:8080 Timeout:30s Debug:true}
}

성능 고려사항

1. 제네릭 vs 인터페이스

// 제네릭 (컴파일 타임 타입 특화)
func SumGeneric[T constraints.Integer](numbers []T) T {
    var sum T
    for _, n := range numbers {
        sum += n
    }
    return sum
}

// 인터페이스 (런타임 타입 변환)
func SumInterface(numbers []interface{}) int {
    sum := 0
    for _, n := range numbers {
        sum += n.(int) // 런타임 타입 단언
    }
    return sum
}

// 벤치마크에서 제네릭이 더 빠름

2. 인라인 최적화

// 간단한 제네릭 함수는 인라인 최적화됨
func Max[T constraints.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

// 컴파일러가 호출 지점에 직접 코드 삽입
x := Max(10, 20) // 인라인됨

3. 코드 크기

// 제네릭은 각 타입에 대해 코드 생성
func Process[T any](value T) { /* ... */ }

// 다음 호출은 별도 코드 생성
Process(42)      // int 버전
Process("hello") // string 버전
Process(3.14)    // float64 버전

// 많은 타입으로 사용 시 바이너리 크기 증가 가능

일반적인 실수

1. 제약 없이 연산자 사용

// ❌ 나쁜 예
func Add[T any](a, b T) T {
    return a + b // 컴파일 에러: + 연산 불가
}

// ✅ 좋은 예
func Add[T constraints.Integer | constraints.Float | ~string](a, b T) T {
    return a + b
}

2. comparable과 Ordered 혼동

// ❌ 나쁜 예
func Max[T comparable](a, b T) T {
    if a > b { // 컴파일 에러: > 연산 불가
        return a
    }
    return b
}

// ✅ 좋은 예
func Max[T constraints.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

3. 제로 값 처리

// ❌ 나쁜 예
func First[T any](slice []T) T {
    return slice[0] // 빈 슬라이스면 패닉
}

// ✅ 좋은 예
func First[T any](slice []T) (T, bool) {
    if len(slice) == 0 {
        var zero T
        return zero, false
    }
    return slice[0], true
}

4. 타입 파라미터 과다 사용

// ❌ 나쁜 예 (과도한 제네릭)
func Print[T any](value T) {
    fmt.Println(value)
}

// ✅ 좋은 예 (제네릭 불필요)
func Print(value any) {
    fmt.Println(value)
}

5. 메서드에 타입 파라미터

// ❌ 불가능 (메서드는 타입 파라미터 추가 불가)
type Container[T any] struct {
    value T
}

func (c Container[T]) Transform[U any](f func(T) U) U {
    return f(c.value) // 컴파일 에러
}

// ✅ 함수로 구현
func Transform[T, U any](c Container[T], f func(T) U) U {
    return f(c.value)
}

6. 인터페이스 타입 단언

// ❌ 나쁜 예
func Process[T any](value T) {
    if str, ok := value.(string); ok { // 컴파일 에러
        fmt.Println(str)
    }
}

// ✅ 좋은 예
func Process[T any](value T) {
    if str, ok := any(value).(string); ok {
        fmt.Println(str)
    }
}

7. 슬라이스와 comparable

// ❌ 나쁜 예
func Contains[T comparable](slice []T, target T) bool {
    // [][]int는 comparable이 아니므로 사용 불가
}

// 사용 시
slices := [][]int{{1}, {2}}
Contains(slices, []int{1}) // 컴파일 에러

베스트 프랙티스

1. 제네릭 사용 시기

// ✅ 제네릭 사용
// - 타입 안전성이 중요한 데이터 구조
// - 알고리즘이 여러 타입에 동일하게 적용
// - 컴파일 타임 타입 체크 필요

type Stack[T any] struct { /* ... */ }
func Map[T, U any](slice []T, f func(T) U) []U { /* ... */ }

// ❌ 제네릭 불필요
// - 단순한 로깅, 출력
// - 타입 정보가 중요하지 않음
// - any로 충분

func Log(values ...any) { /* ... */ }

2. 명확한 제약 정의

// ✅ 좋은 예: 의미 있는 제약 이름
type Numeric interface {
    constraints.Integer | constraints.Float
}

type Addable interface {
    ~int | ~float64 | ~string
}

func Sum[T Numeric](numbers []T) T { /* ... */ }
func Concat[T Addable](items []T) T { /* ... */ }

3. 타입 추론 활용

// ✅ 타입 추론 활용 (간결)
result := Map([]int{1, 2, 3}, func(n int) string {
    return fmt.Sprintf("%d", n)
})

// ❌ 불필요한 타입 명시
result := Map[int, string]([]int{1, 2, 3}, func(n int) string {
    return fmt.Sprintf("%d", n)
})

4. 에러 처리

// ✅ 좋은 예
func Find[T any](slice []T, predicate func(T) bool) (T, error) {
    for _, item := range slice {
        if predicate(item) {
            return item, nil
        }
    }
    var zero T
    return zero, fmt.Errorf("not found")
}

5. 문서화

// Sum calculates the sum of all numbers in the slice.
// T must be a numeric type (integer or float).
//
// Example:
//
//	numbers := []int{1, 2, 3, 4, 5}
//	total := Sum(numbers) // 15
func Sum[T Numeric](numbers []T) T {
    var sum T
    for _, n := range numbers {
        sum += n
    }
    return sum
}

6. 제네릭 팩토리 함수

// ✅ 생성자 패턴
func NewCache[K comparable, V any](ttl time.Duration) *Cache[K, V] {
    return &Cache[K, V]{
        items: make(map[K]V),
        ttl:   ttl,
    }
}

// 사용
userCache := NewCache[int, User](5 * time.Minute)

7. 타입 별칭 활용

// ✅ 복잡한 제네릭 타입 별칭
type StringMap[V any] = Map[string, V]
type IntCache[V any] = Cache[int, V]

// 사용
var userMap StringMap[User]
var sessionCache IntCache[Session]

8. 인터페이스와 결합

// ✅ 제네릭 + 인터페이스
type Serializable interface {
    Marshal() ([]byte, error)
    Unmarshal([]byte) error
}

type Repository[T Serializable] struct {
    items map[string]T
}

func (r *Repository[T]) Save(id string, item T) error {
    data, err := item.Marshal()
    if err != nil {
        return err
    }
    // Save data...
    return nil
}

정리

  • 기본: any, comparable, 인라인 제약
  • 타입 제약: Union, ~, 인터페이스, constraints 패키지
  • 데이터 구조: Stack, Queue, LinkedList, Tree, Map
  • 다중 타입: Pair, Result, Either
  • 타입 추론: 인자, 반환, 슬라이스
  • 실전: Cache, Optional, Reducer, Validation, Builder
  • 성능: 제네릭 vs 인터페이스, 인라인 최적화
  • 실수: 제약 누락, comparable/Ordered 혼동, 제로 값, 과다 사용
  • 베스트: 적절한 사용, 명확한 제약, 타입 추론, 문서화