본문으로 건너뛰기

실전 고수 팁 — 복합 타입

슬라이스 메모리 누수 패턴

슬라이스가 내부 배열을 공유하는 특성 때문에 대용량 배열을 참조하는 작은 슬라이스가 GC를 막는 상황이 발생합니다.

package main

import "fmt"

// 문제: 1만 개 데이터 중 3개만 필요하지만 전체 배열이 메모리에 남음
func loadHugeData() []int {
huge := make([]int, 10000)
for i := range huge {
huge[i] = i
}
return huge
}

func badPattern() []int {
data := loadHugeData()
return data[:3] // 10000개짜리 배열 전체가 GC 대상 외
}

func goodPattern() []int {
data := loadHugeData()
result := make([]int, 3)
copy(result, data[:3]) // 독립적인 3개짜리 배열만 유지
return result
}

// 포인터 슬라이스의 누수 — nil 처리 필수
func removeElement(s []*int, i int) []*int {
// 나쁜 패턴: 삭제된 슬롯이 포인터를 계속 보유
// return append(s[:i], s[i+1:]...)

// 좋은 패턴: 삭제 전 nil로 초기화
s[len(s)-1] = nil // GC가 수집할 수 있도록
copy(s[i:], s[i+1:])
return s[:len(s)-1]
}

func main() {
bad := badPattern()
good := goodPattern()
fmt.Println(bad, good)
}

슬라이스 사전 할당으로 성능 올리기

append가 반복적으로 재할당을 일으키면 성능이 떨어집니다. 결과 크기를 미리 알 수 있다면 make로 용량을 지정하세요.

package main

import (
"fmt"
"time"
)

func withoutPrealloc(n int) []int {
var result []int
for i := 0; i < n; i++ {
result = append(result, i*2)
}
return result
}

func withPrealloc(n int) []int {
result := make([]int, 0, n) // 용량 미리 지정
for i := 0; i < n; i++ {
result = append(result, i*2)
}
return result
}

func withLength(n int) []int {
result := make([]int, n) // 길이와 용량 모두 지정
for i := 0; i < n; i++ {
result[i] = i * 2
}
return result
}

func benchmark(name string, fn func(int) []int, n int) {
start := time.Now()
for i := 0; i < 1000; i++ {
fn(n)
}
fmt.Printf("%-20s %v\n", name, time.Since(start))
}

func main() {
const N = 10000
benchmark("without prealloc", withoutPrealloc, N)
benchmark("with prealloc", withPrealloc, N)
benchmark("with length", withLength, N)
// with length가 가장 빠름
}

맵 vs 구조체 선택 기준

상황추천 타입
키가 컴파일 타임에 확정구조체
키가 런타임에 동적으로 결정
JSON 직렬화, DB 매핑구조체 (태그 활용)
빈번한 키 추가·삭제
타입 안전성 중요구조체
설정 파일, 플러그인 메타데이터
package main

import "fmt"

// 구조체가 적합 — 필드가 고정됨
type Config struct {
Host string
Port int
Debug bool
Timeout int
}

// 맵이 적합 — 키가 동적
type DynamicConfig map[string]any

func main() {
// 구조체: IDE 자동완성, 컴파일 타임 타입 체크
cfg := Config{Host: "localhost", Port: 8080, Debug: true, Timeout: 30}
fmt.Println(cfg.Host) // 오타 시 컴파일 에러

// 맵: 런타임에 키 추가 가능
dyn := DynamicConfig{
"host": "localhost",
"port": 8080,
"version": "2.1.0",
}
dyn["new_feature"] = true // 구조체로는 불가
fmt.Println(dyn["host"])
}

구조체 임베딩으로 기능 확장

임베딩은 상속이 아닌 구성(composition) 입니다. has-a 관계를 표현합니다.

package main

import (
"fmt"
"sync"
"time"
)

// 공통 타임스탬프 기능을 임베딩으로 재사용
type Timestamps struct {
CreatedAt time.Time
UpdatedAt time.Time
}

func (t *Timestamps) Touch() {
t.UpdatedAt = time.Now()
}

type Article struct {
Timestamps // 임베딩
Title string
Content string
}

type Comment struct {
Timestamps // 동일 임베딩 재사용
ArticleID int
Body string
}

// 동시성 안전 구조체 — sync.Mutex 임베딩
type SafeCounter struct {
sync.Mutex // Mutex 임베딩
count int
}

func (sc *SafeCounter) Increment() {
sc.Lock() // sync.Mutex.Lock() 승격
defer sc.Unlock()
sc.count++
}

func (sc *SafeCounter) Value() int {
sc.RLock() // 컴파일 에러! sync.Mutex는 RLock 없음
// sync.RWMutex를 임베딩해야 RLock 사용 가능
_ = sc
return sc.count
}

func main() {
a := Article{
Title: "Go 구조체 임베딩",
Content: "임베딩은 구성의 수단입니다.",
}
a.CreatedAt = time.Now()
a.Touch() // Timestamps.Touch() 직접 호출

fmt.Printf("글: %s, 수정: %s\n", a.Title, a.UpdatedAt.Format("15:04:05"))
}

제네릭 슬라이스 유틸 (Go 1.18+)

반복되는 슬라이스 조작을 타입 파라미터로 일반화할 수 있습니다. Go 1.21부터는 slices 표준 패키지도 제공됩니다.

package main

import (
"fmt"
"slices" // Go 1.21+
)

// Filter — 조건을 만족하는 요소만 반환
func Filter[T any](s []T, fn func(T) bool) []T {
result := make([]T, 0)
for _, v := range s {
if fn(v) {
result = append(result, v)
}
}
return result
}

// Map — 각 요소를 변환
func MapSlice[T, U any](s []T, fn func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = fn(v)
}
return result
}

// Reduce — 슬라이스를 하나의 값으로 축약
func Reduce[T, U any](s []T, init U, fn func(U, T) U) U {
acc := init
for _, v := range s {
acc = fn(acc, v)
}
return acc
}

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

// 짝수 필터
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
fmt.Println(evens) // [2 4 6 8 10]

// 제곱 변환
squares := MapSlice(nums, func(n int) int { return n * n })
fmt.Println(squares) // [1 4 9 16 25 36 49 64 81 100]

// 합산
sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })
fmt.Println(sum) // 55

// Go 1.21 slices 표준 패키지
words := []string{"banana", "apple", "cherry", "date"}
slices.Sort(words)
fmt.Println(words) // [apple banana cherry date]
fmt.Println(slices.Contains(words, "apple")) // true
idx, _ := slices.BinarySearch(words, "cherry")
fmt.Println(idx) // 2
}

맵 초기화 패턴 모음

package main

import "fmt"

func main() {
// 패턴 1: 존재하면 증가, 없으면 초기화 (제로값 활용)
counter := make(map[string]int)
words := []string{"go", "is", "great", "go", "is"}
for _, w := range words {
counter[w]++ // 없는 키도 제로값(0)에서 시작하므로 안전
}
fmt.Println(counter) // map[go:2 great:1 is:2]

// 패턴 2: 슬라이스 값에 안전하게 append
groups := make(map[string][]string)
data := [][2]string{{"A", "alice"}, {"B", "bob"}, {"A", "carol"}}
for _, d := range data {
groups[d[0]] = append(groups[d[0]], d[1]) // nil 슬라이스에 append 가능
}
fmt.Println(groups) // map[A:[alice carol] B:[bob]]

// 패턴 3: 집합(Set) 구현
set := make(map[string]struct{})
for _, item := range []string{"a", "b", "a", "c", "b"} {
set[item] = struct{}{} // 빈 구조체로 메모리 절약
}
fmt.Println(len(set)) // 3 (중복 제거)
_, inSet := set["a"]
fmt.Println(inSet) // true

// 패턴 4: 맵 복사 (얕은 복사)
original := map[string]int{"a": 1, "b": 2}
clone := make(map[string]int, len(original))
for k, v := range original {
clone[k] = v
}
clone["c"] = 3
fmt.Println(original) // map[a:1 b:2] — 영향 없음
}

구조체 생성자 패턴

Go에는 생성자 문법이 없습니다. 관례적으로 New 함수를 사용해 유효성 검사와 기본값 설정을 처리합니다.

package main

import (
"errors"
"fmt"
)

type Server struct {
host string // 소문자 = 비공개 필드
port int
timeout int
}

// 생성자 함수 — 유효성 검사 포함
func NewServer(host string, port int) (*Server, error) {
if host == "" {
return nil, errors.New("host는 비어있을 수 없습니다")
}
if port < 1 || port > 65535 {
return nil, fmt.Errorf("유효하지 않은 포트: %d", port)
}
return &Server{
host: host,
port: port,
timeout: 30, // 기본값
}, nil
}

func (s *Server) Address() string {
return fmt.Sprintf("%s:%d", s.host, s.port)
}

func main() {
s, err := NewServer("localhost", 8080)
if err != nil {
fmt.Println("에러:", err)
return
}
fmt.Println(s.Address()) // localhost:8080

_, err = NewServer("", 8080)
fmt.Println(err) // host는 비어있을 수 없습니다
}