go
2026년 3월 4일· 9분 읽기

Golang Generics (3) - 실전 예제 모음

#golang
#go
#generics
#제네릭
#stack
#queue
#filter
#map
#reduce
#data-structure
#자료구조
#고랭
📚시리즈: Golang Generics

1편에서 기본 문법을, 2편에서 Type Constraint를 다뤘다. 이번 편에서는 Generics를 활용한 실전 예제들을 구현해본다. 자료구조, 유틸 함수, 함수형 패턴, 그리고 Go 1.21+ 표준 라이브러리까지 살펴본다.

1. Generic 자료구조 구현

1.1 Generic Stack

LIFO(Last In, First Out) 구조의 Stack을 Generics로 구현한다.

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

func NewStack[T any]() *Stack[T] {
    return &Stack[T]{items: make([]T, 0)}
}

func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
    var zero T // Generic에서 제로값 반환용 관용 패턴
    if len(s.items) == 0 {
        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) {
    var zero T
    if len(s.items) == 0 {
        return zero, false
    }
    return s.items[len(s.items)-1], true
}

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

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

하나의 구현으로 int, string 등 어떤 타입에도 사용할 수 있다.

// int 스택
intStack := NewStack[int]()
intStack.Push(1)
intStack.Push(2)
intStack.Push(3)
val, _ := intStack.Pop()  // 3

// string 스택
strStack := NewStack[string]()
strStack.Push("hello")
strStack.Push("world")
val, _ := strStack.Pop()  // "world"

var zero T는 Generic 코드에서 제로값을 반환할 때 사용하는 관용적 패턴이다. T의 제로값은 int0, string이면 "", 포인터면 nil이 된다.

1.2 Generic Queue

FIFO(First In, First Out) 구조의 Queue를 구현한다.

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

func NewQueue[T any]() *Queue[T] {
    return &Queue[T]{items: make([]T, 0)}
}

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

func (q *Queue[T]) Dequeue() (T, bool) {
    var zero T
    if len(q.items) == 0 {
        return zero, false
    }
    item := q.items[0]
    q.items = q.items[1:] // 앞 요소 제거 (간단한 구현, 대량 데이터시 ring buffer 고려)
    return item, true
}

func (q *Queue[T]) Front() (T, bool) {
    var zero T
    if len(q.items) == 0 {
        return zero, false
    }
    return q.items[0], true
}
q := NewQueue[string]()
q.Enqueue("first")
q.Enqueue("second")
q.Enqueue("third")

for !q.IsEmpty() {
    val, _ := q.Dequeue()
    fmt.Println(val)
}
// first
// second
// third

2. Generic 유틸 함수

2.1 Min / Max 함수

constraints.Ordered를 활용하면 모든 비교 가능한 타입에 대해 동작하는 Min/Max를 구현할 수 있다.

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

func MinOf[T constraints.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func MaxOf[T constraints.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

MinOf(10, 20)              // 10
MaxOf(3.14, 2.71)          // 3.14
MinOf("apple", "banana")   // "apple"

슬라이스에서 최솟값/최댓값을 찾는 함수도 동일한 패턴으로 구현한다.

func MinSlice[T constraints.Ordered](s []T) (T, bool) {
    var zero T
    if len(s) == 0 {
        return zero, false
    }
    result := s[0]
    for _, v := range s[1:] {
        if v < result {
            result = v
        }
    }
    return result, true
}

minVal, _ := MinSlice([]int{5, 3, 8, 1, 9})           // 1
maxVal, _ := MaxSlice([]int{5, 3, 8, 1, 9})           // 9
minStr, _ := MinSlice([]string{"banana", "apple", "cherry"}) // "apple"

3. Slice 유틸 함수 (함수형 패턴)

3.1 Filter

조건을 만족하는 요소만 반환한다.

func Filter[T any](s []T, predicate func(T) bool) []T {
    result := make([]T, 0) // 결과 크기를 미리 알 수 없으므로 빈 슬라이스로 시작
    for _, v := range s {
        if predicate(v) { // 조건 함수가 true를 반환하면 포함
            result = append(result, v)
        }
    }
    return result
}

// 짝수만 필터링
evens := Filter([]int{1, 2, 3, 4, 5, 6}, func(n int) bool {
    return n%2 == 0
})
// [2 4 6]

// 길이 3 이상인 문자열만
long := Filter([]string{"go", "java", "py", "rust"}, func(s string) bool {
    return len(s) >= 3
})
// [java rust]

3.2 Map

슬라이스의 각 요소를 변환한다. 입력과 출력의 타입이 다를 수 있으므로 타입 파라미터가 2개 필요하다.

// T → U 변환: 입력과 출력 타입이 다를 수 있어 타입 파라미터 2개 필요
func MapSlice[T, U any](s []T, transform func(T) U) []U {
    result := make([]U, len(s)) // 입력과 1:1 대응이므로 길이를 미리 할당 (append 불필요)
    for i, v := range s {
        result[i] = transform(v)
    }
    return result
}

// int -> int (2배)
doubled := MapSlice([]int{1, 2, 3}, func(n int) int { return n * 2 })
// [2 4 6]

// string -> string (대문자)
uppered := MapSlice([]string{"hello", "world"}, func(s string) string {
    return strings.ToUpper(s)
})
// [HELLO WORLD]

// int -> string (타입 변환)
strs := MapSlice([]int{1, 2, 3}, func(n int) string {
    return fmt.Sprintf("#%d", n)
})
// [#1 #2 #3]

3.3 Reduce

슬라이스를 하나의 값으로 축약한다.

func Reduce[T, U any](s []T, initial U, accumulator func(U, T) U) U {
    result := initial // 누적값의 시작점 (합계면 0, 문자열 합치기면 "")
    for _, v := range s {
        result = accumulator(result, v) // 이전 누적값과 현재 요소로 새 누적값 계산
    }
    return result
}

// 합계
total := Reduce([]int{1, 2, 3, 4, 5}, 0, func(acc, n int) int {
    return acc + n
})
// 15

// 문자열 합치기
joined := Reduce([]string{"Go", "Generics", "Rock"}, "", func(acc, s string) string {
    if acc == "" { return s }
    return acc + " " + s
})
// "Go Generics Rock"

3.4 Filter + Map + Reduce 조합

함수형 패턴의 진가는 조합에서 발휘된다.

nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

// 파이프라인: Filter → Map → Reduce (각 단계의 결과가 다음 단계의 입력)
evens := Filter(nums, func(n int) bool { return n%2 == 0 })       // [2 4 6 8 10]
doubled := MapSlice(evens, func(n int) int { return n * 2 })       // [4 8 12 16 20]
total := Reduce(doubled, 0, func(acc, n int) int { return acc + n }) // 60

4. Generic Map 헬퍼

4.1 Values 추출

func MapValues[K comparable, V any](m map[K]V) []V {
    values := make([]V, 0, len(m)) // len=0, cap=len(m): 크기는 알지만 순서가 없으므로 append 사용
    for _, v := range m {
        values = append(values, v)
    }
    return values
}

m := map[string]int{"a": 1, "b": 2, "c": 3}
values := MapValues(m)  // [1 2 3] (순서 비보장)

4.2 Merge

두 map을 합친다. 키가 겹치면 두 번째 map의 값이 우선한다.

func MapMerge[K comparable, V any](m1, m2 map[K]V) map[K]V {
    result := make(map[K]V, len(m1)+len(m2)) // 최대 크기로 미리 할당 (rehash 방지)
    for k, v := range m1 {
        result[k] = v
    }
    for k, v := range m2 {
        result[k] = v // 키 충돌 시 m2 값이 m1을 덮어씀
    }
    return result
}

m1 := map[string]int{"a": 1, "b": 2}
m2 := map[string]int{"b": 20, "c": 3}
merged := MapMerge(m1, m2)
// {"a": 1, "b": 20, "c": 3}

4.3 Filter

조건을 만족하는 key-value 쌍만 반환한다.

func MapFilter[K comparable, V any](m map[K]V, predicate func(K, V) bool) map[K]V {
    result := make(map[K]V)
    for k, v := range m {
        if predicate(k, v) {
            result[k] = v
        }
    }
    return result
}

scores := map[string]int{"alice": 85, "bob": 42, "carol": 91, "dave": 67}
passed := MapFilter(scores, func(k string, v int) bool {
    return v >= 70
})
// {"alice": 85, "carol": 91}

5. 표준 라이브러리 활용 (Go 1.21+)

Go 1.21부터 Generics를 활용한 표준 패키지 slices, maps, cmp가 추가되었다. 앞서 직접 구현한 유틸 함수들의 상당수가 이미 표준 라이브러리에 포함되어 있다.

5.1 slices 패키지

import "slices"

// 정렬
nums := []int{5, 3, 8, 1, 9, 2}
slices.Sort(nums)
// [1 2 3 5 8 9]

// 포함 여부
slices.Contains([]int{1, 2, 3, 4, 5}, 3)   // true
slices.Contains([]int{1, 2, 3, 4, 5}, 6)   // false

// 최솟값 / 최댓값
slices.Min([]int{5, 3, 8, 1, 9})  // 1
slices.Max([]int{5, 3, 8, 1, 9})  // 9

커스텀 정렬도 SortFunc로 간결하게 표현할 수 있다.

// 절대값 기준 정렬: 비교 함수는 a<b이면 음수, a==b이면 0, a>b이면 양수 반환
nums := []int{-5, 3, -1, 8, -2}
slices.SortFunc(nums, func(a, b int) int {
    absA, absB := a, b
    if absA < 0 { absA = -absA }
    if absB < 0 { absB = -absB }
    return absA - absB // 절대값 차이로 정렬 순서 결정
})
// [-1 -2 3 -5 8]

5.2 maps 패키지

import "maps"

// Clone - 깊은 복사
original := map[string]int{"a": 1, "b": 2, "c": 3}
cloned := maps.Clone(original)
cloned["d"] = 4  // clone 수정해도 원본 영향 없음

// Equal - 두 map 비교
m1 := map[string]int{"a": 1, "b": 2}
m2 := map[string]int{"a": 1, "b": 2}
m3 := map[string]int{"a": 1, "b": 3}
maps.Equal(m1, m2)  // true
maps.Equal(m1, m3)  // false

5.3 cmp 패키지

import "cmp"

// Compare - 두 값 비교 (-1, 0, 1 반환)
cmp.Compare(1, 2)  // -1 (1 < 2)
cmp.Compare(2, 2)  //  0 (2 == 2)
cmp.Compare(3, 2)  //  1 (3 > 2)

cmp.Or는 첫 번째 non-zero 값을 반환한다. 설정값 우선순위 패턴에 매우 유용하다.

// 사용자 설정 > 환경 변수 > 기본값
userConfig := ""
envConfig := ""
defaultConfig := "localhost:8080"

addr := cmp.Or(userConfig, envConfig, defaultConfig) // 제로값("")이 아닌 첫 번째 값 반환
// "localhost:8080"

envConfig = "0.0.0.0:9090"
addr = cmp.Or(userConfig, envConfig, defaultConfig)
// "0.0.0.0:9090" (환경 변수가 설정된 경우)

6. 직접 구현 vs 표준 라이브러리

기능직접 구현표준 라이브러리 (Go 1.21+)
Containscontains[T comparable]slices.Contains
Min/MaxMinOf[T constraints.Ordered]slices.Min, slices.Max
Sort-slices.Sort, slices.SortFunc
Map Clone-maps.Clone
Map Equal-maps.Equal
Filter/Map/Reduce직접 구현 필요Go 1.23+ slices.Collect 등 일부 추가

실무 권장사항: Go 1.21 이상을 사용한다면 slices, maps, cmp 표준 패키지를 우선 사용하자. 직접 구현은 표준 라이브러리에 없는 기능(Filter, Map, Reduce 등)이나 학습 목적으로 활용하면 된다.

7. 마무리

이번 편에서 다룬 Generic 패턴들을 요약한다.

패턴핵심 포인트
Stack / Queuevar zero T로 제로값 반환, 메서드에서 struct 타입 파라미터 활용
Min / Maxconstraints.Ordered로 비교 가능한 모든 타입 지원
Filter / Map / Reducefunc(T) bool, func(T) U 콜백 패턴, 함수형 조합
Map 헬퍼K comparable, V any 패턴으로 map key 제약
표준 라이브러리Go 1.21+ slices, maps, cmp 패키지 적극 활용

다음 편에서는 Generics vs Interface 비교성능 벤치마크를 다룬다.

8. 참고

관련 글