Learning
레슨 8 / 10·3개 토픽

포인터와 제네릭

포인터 기초

포인터는 값이 저장된 메모리 주소를 담는 변수입니다. &로 변수의 주소를 얻고, *로 포인터가 가리키는 값에 접근(역참조)합니다. Go에서는 포인터 연산이 불가하므로 C보다 안전하게 사용할 수 있습니다.

go
package main

import "fmt"

func main() {
    // 포인터 기본
    x := 42
    p := &x         // p는 x의 주소를 가리킴
    fmt.Println(*p) // 42 (역참조)
    *p = 100        // 포인터를 통해 값 변경
    fmt.Println(x)  // 100

    // nil 포인터
    var ptr *int    // 초기값은 nil
    fmt.Println(ptr == nil) // true

    // new()로 포인터 생성
    q := new(int)   // *int 타입, 제로값(0)으로 초기화
    *q = 200
    fmt.Println(*q) // 200
}

값 리시버 vs 포인터 리시버

go
package main

import "fmt"

type Counter struct {
    count int
}

// 값 리시버 — 복사본을 수정 (원본 변경 안 됨)
func (c Counter) ValueIncrement() {
    c.count++ // 원본에 영향 없음
}

// 포인터 리시버 — 원본을 직접 수정
func (c *Counter) PointerIncrement() {
    c.count++ // 원본 변경됨
}

func (c Counter) GetCount() int {
    return c.count
}

func main() {
    c := Counter{count: 0}

    c.ValueIncrement()
    fmt.Println(c.GetCount()) // 0 (변경 안 됨)

    c.PointerIncrement()
    fmt.Println(c.GetCount()) // 1 (변경됨)

    // 함수에 포인터 전달
    addTen := func(val *int) {
        *val += 10
    }
    num := 5
    addTen(&num)
    fmt.Println(num) // 15
}

제네릭 (Go 1.18+)

Go 1.18부터 제네릭(타입 매개변수)을 지원합니다. 함수와 타입에 [T constraint] 형태로 타입 매개변수를 선언하며, any(모든 타입)와 comparable(==, != 비교 가능한 타입) 등의 제약 조건을 사용합니다.

go
package main

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

// 제네릭 함수 — 모든 숫자 타입의 최솟값
func Min[T constraints.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

// 제네릭 함수 — 슬라이스에서 요소 찾기
func Contains[T comparable](slice []T, target T) bool {
    for _, v := range slice {
        if v == target {
            return true
        }
    }
    return false
}

// 제네릭 타입 — 범용 스택
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 main() {
    fmt.Println(Min(3, 7))         // 3
    fmt.Println(Min("apple", "banana")) // apple

    fmt.Println(Contains([]int{1, 2, 3}, 2))     // true
    fmt.Println(Contains([]string{"a", "b"}, "c")) // false

    s := Stack[int]{}
    s.Push(10)
    s.Push(20)
    val, ok := s.Pop()
    fmt.Println(val, ok) // 20 true
}
  • & — 변수의 메모리 주소를 얻는 연산자
  • * — 포인터가 가리키는 값에 접근하는 역참조 연산자
  • nil — 포인터의 제로 값 (아무것도 가리키지 않음)
  • 값 리시버 — 구조체의 복사본에서 동작 (원본 변경 불가)
  • 포인터 리시버 — 구조체 원본을 직접 수정
  • [T any] — 모든 타입을 받는 제네릭 타입 매개변수
  • [T comparable]==, != 비교 가능한 타입만 허용
💡

구조체가 크거나 메서드에서 필드를 수정해야 한다면 포인터 리시버를 사용하세요. 일관성을 위해 한 타입의 모든 메서드에 같은 리시버 타입(값 또는 포인터)을 사용하는 것이 관례입니다.