[Go] generics
Updated:
개요
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 혼동, 제로 값, 과다 사용
- 베스트: 적절한 사용, 명확한 제약, 타입 추론, 문서화