Updated:

13 minute read

개요

슬라이스(slice)는 동적 크기를 가진 배열의 유연한 뷰입니다.

주요 특징:

  • 동적 크기: 런타임에 크기 변경 가능
  • 참조 타입: 내부 배열에 대한 참조 (포인터, 길이, 용량)
  • 제로값은 nil: nil 슬라이스도 안전하게 사용 가능
  • make() 또는 슬라이스 리터럴로 생성
  • append()로 요소 추가 (자동 확장)
  • copy()로 복사
  • 배열보다 훨씬 많이 사용됨

슬라이스의 내부 구조

// 슬라이스의 런타임 표현
type slice struct {
    ptr *ElementType  // 내부 배열의 포인터
    len int           // 현재 길이
    cap int           // 용량 (최대 길이)
}
func sliceInternals() {
    // 슬라이스 생성
    s := []int{1, 2, 3, 4, 5}
    
    // 길이: 현재 요소 개수
    fmt.Println("Length:", len(s))    // 5
    
    // 용량: 내부 배열의 크기
    fmt.Println("Capacity:", cap(s))  // 5
    
    // 슬라이싱으로 뷰 생성
    s2 := s[1:4]  // [2, 3, 4]
    fmt.Println("s2:", s2)
    fmt.Println("s2 len:", len(s2))   // 3
    fmt.Println("s2 cap:", cap(s2))   // 4 (s[1]부터 끝까지)
    
    // 같은 내부 배열 공유
    s2[0] = 999
    fmt.Println("s:", s)   // [1 999 3 4 5]
    fmt.Println("s2:", s2) // [999 3 4]
}

슬라이스 생성 방법

1. nil 슬라이스

func nilSlice() {
    var s []int
    
    fmt.Println(s)         // []
    fmt.Println(s == nil)  // true
    fmt.Println(len(s))    // 0
    fmt.Println(cap(s))    // 0
    
    // nil 슬라이스도 안전하게 사용 가능
    s = append(s, 1, 2, 3)
    fmt.Println(s)         // [1 2 3]
}

2. 빈 슬라이스

func emptySlice() {
    // 방법 1: 리터럴
    s1 := []int{}
    fmt.Println(s1 == nil) // false
    fmt.Println(len(s1))   // 0
    
    // 방법 2: make
    s2 := make([]int, 0)
    fmt.Println(s2 == nil) // false
    fmt.Println(len(s2))   // 0
    
    // nil vs 빈 슬라이스는 대부분의 경우 동일하게 동작
    // JSON 직렬화 등에서 차이 발생 가능
}

3. make로 생성

func makeSlice() {
    // make([]Type, length, capacity)
    
    // 길이 5, 용량 5
    s1 := make([]int, 5)
    fmt.Println(s1)        // [0 0 0 0 0]
    fmt.Println(len(s1))   // 5
    fmt.Println(cap(s1))   // 5
    
    // 길이 3, 용량 5
    s2 := make([]int, 3, 5)
    fmt.Println(s2)        // [0 0 0]
    fmt.Println(len(s2))   // 3
    fmt.Println(cap(s2))   // 5
    
    // append 시 재할당 없이 확장
    s2 = append(s2, 1, 2)
    fmt.Println(s2)        // [0 0 0 1 2]
    fmt.Println(len(s2))   // 5
    fmt.Println(cap(s2))   // 5
}

4. 슬라이스 리터럴

func sliceLiteral() {
    // 초기값과 함께 생성
    s1 := []int{1, 2, 3, 4, 5}
    
    // 인덱스 지정 초기화
    s2 := []int{0: 10, 2: 20, 5: 50}
    fmt.Println(s2) // [10 0 20 0 0 50]
    
    // 문자열 슬라이스
    s3 := []string{"Go", "Python", "Java"}
    
    // 구조체 슬라이스
    type Person struct {
        Name string
        Age  int
    }
    s4 := []Person{
        {"Alice", 25},
        {"Bob", 30},
    }
    fmt.Printf("%+v\n", s4)
}

5. 배열에서 슬라이스 생성

func arrayToSlice() {
    arr := [5]int{1, 2, 3, 4, 5}
    
    // 전체 슬라이스
    s1 := arr[:]
    fmt.Println(s1) // [1 2 3 4 5]
    
    // 부분 슬라이스
    s2 := arr[1:4]
    fmt.Println(s2) // [2 3 4]
    
    // 슬라이스는 배열을 참조
    s1[0] = 100
    fmt.Println(arr) // [100 2 3 4 5]
}

슬라이싱 연산

1. 기본 슬라이싱

func slicingBasics() {
    s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    
    // s[low:high] - low부터 high-1까지
    fmt.Println(s[2:5])   // [2 3 4]
    
    // 처음부터
    fmt.Println(s[:5])    // [0 1 2 3 4]
    
    // 끝까지
    fmt.Println(s[5:])    // [5 6 7 8 9]
    
    // 전체
    fmt.Println(s[:])     // [0 1 2 3 4 5 6 7 8 9]
    
    // 빈 슬라이스
    fmt.Println(s[5:5])   // []
}

2. Full Slice Expression (3-인덱스)

func fullSliceExpression() {
    s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    
    // s[low:high:max] - 용량 제한
    s2 := s[2:5:7]
    fmt.Println(s2)        // [2 3 4]
    fmt.Println(len(s2))   // 3
    fmt.Println(cap(s2))   // 5 (7-2)
    
    // 용량을 제한하여 원본 슬라이스 보호
    s3 := s[2:5:5]  // 용량 3
    s3 = append(s3, 99)
    // s3이 확장되면서 새 배열 할당, s는 영향받지 않음
    fmt.Println(s)         // [0 1 2 3 4 5 6 7 8 9]
    fmt.Println(s3)        // [2 3 4 99]
}

Append 함수

1. 기본 Append

func appendBasics() {
    var s []int
    fmt.Println(s, len(s), cap(s)) // [] 0 0
    
    // 단일 요소 추가
    s = append(s, 1)
    fmt.Println(s, len(s), cap(s)) // [1] 1 1
    
    // 여러 요소 추가
    s = append(s, 2, 3, 4)
    fmt.Println(s, len(s), cap(s)) // [1 2 3 4] 4 4
    
    // 다른 슬라이스 추가 (언팩)
    s2 := []int{5, 6, 7}
    s = append(s, s2...)
    fmt.Println(s, len(s), cap(s)) // [1 2 3 4 5 6 7] 7 8
}

2. Append 용량 확장

func appendCapacity() {
    s := make([]int, 0, 2)
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
    
    s = append(s, 1)
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
    // len=1 cap=2 [1]
    
    s = append(s, 2)
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
    // len=2 cap=2 [1 2]
    
    // 용량 초과 시 재할당 (일반적으로 2배)
    s = append(s, 3)
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
    // len=3 cap=4 [1 2 3]
    
    s = append(s, 4, 5, 6)
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
    // len=6 cap=8 [1 2 3 4 5 6]
}

3. Append 반환값 할당 중요성

func appendReturnValue() {
    s1 := []int{1, 2, 3}
    s2 := s1
    
    // ❌ 잘못된 사용: 반환값 무시
    append(s1, 4)
    fmt.Println(s1) // [1 2 3] (변경 안 됨)
    
    // ✅ 올바른 사용: 반환값 할당
    s1 = append(s1, 4)
    fmt.Println(s1) // [1 2 3 4]
    
    // 주의: s1과 s2가 다른 배열을 가리킬 수 있음
    fmt.Println(s2) // [1 2 3] 또는 [1 2 3 4] (용량에 따라 다름)
}

Copy 함수

func copySlice() {
    src := []int{1, 2, 3, 4, 5}
    
    // 같은 크기로 복사
    dst1 := make([]int, len(src))
    n := copy(dst1, src)
    fmt.Printf("Copied %d elements: %v\n", n, dst1)
    // Copied 5 elements: [1 2 3 4 5]
    
    // 작은 크기로 복사
    dst2 := make([]int, 3)
    n = copy(dst2, src)
    fmt.Printf("Copied %d elements: %v\n", n, dst2)
    // Copied 3 elements: [1 2 3]
    
    // 큰 크기로 복사
    dst3 := make([]int, 7)
    n = copy(dst3, src)
    fmt.Printf("Copied %d elements: %v\n", n, dst3)
    // Copied 5 elements: [1 2 3 4 5 0 0]
    
    // 원본 수정해도 복사본 영향 없음
    src[0] = 999
    fmt.Println("src:", src)   // [999 2 3 4 5]
    fmt.Println("dst1:", dst1) // [1 2 3 4 5]
}

슬라이스 연산

1. 요소 삭제

func deleteElements() {
    s := []int{1, 2, 3, 4, 5}
    
    // 인덱스 2 삭제 (순서 유지)
    i := 2
    s = append(s[:i], s[i+1:]...)
    fmt.Println(s) // [1 2 4 5]
    
    // 인덱스 1 삭제 (빠른 방법, 순서 변경)
    s = []int{1, 2, 3, 4, 5}
    i = 1
    s[i] = s[len(s)-1]  // 마지막 요소로 대체
    s = s[:len(s)-1]     // 마지막 제거
    fmt.Println(s) // [1 5 3 4]
    
    // 범위 삭제
    s = []int{1, 2, 3, 4, 5}
    s = append(s[:1], s[3:]...)
    fmt.Println(s) // [1 4 5]
}

2. 요소 삽입

func insertElements() {
    s := []int{1, 2, 5, 6}
    
    // 인덱스 2에 3, 4 삽입
    i := 2
    s = append(s[:i], append([]int{3, 4}, s[i:]...)...)
    fmt.Println(s) // [1 2 3 4 5 6]
    
    // 더 효율적인 방법
    s = []int{1, 2, 5, 6}
    i = 2
    s = append(s, 0, 0)           // 공간 확보
    copy(s[i+2:], s[i:])          // 뒤로 이동
    s[i], s[i+1] = 3, 4           // 삽입
    fmt.Println(s) // [1 2 3 4 5 6]
}

3. 필터링

func filterSlice() {
    numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    
    // 짝수만 필터
    evens := []int{}
    for _, n := range numbers {
        if n%2 == 0 {
            evens = append(evens, n)
        }
    }
    fmt.Println(evens) // [2 4 6 8 10]
    
    // In-place 필터링 (메모리 효율적)
    numbers = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    n := 0
    for _, x := range numbers {
        if x%2 == 0 {
            numbers[n] = x
            n++
        }
    }
    numbers = numbers[:n]
    fmt.Println(numbers) // [2 4 6 8 10]
}

4. 역순 정렬

func reverseSlice() {
    s := []int{1, 2, 3, 4, 5}
    
    // In-place 역순
    for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
        s[i], s[j] = s[j], s[i]
    }
    fmt.Println(s) // [5 4 3 2 1]
}

다차원 슬라이스

func multiDimensionalSlice() {
    // 2D 슬라이스 생성
    rows, cols := 3, 4
    matrix := make([][]int, rows)
    for i := range matrix {
        matrix[i] = make([]int, cols)
    }
    
    // 값 할당
    for i := 0; i < rows; i++ {
        for j := 0; j < cols; j++ {
            matrix[i][j] = i*cols + j
        }
    }
    
    fmt.Println(matrix)
    // [[0 1 2 3] [4 5 6 7] [8 9 10 11]]
    
    // 초기값과 함께 생성
    matrix2 := [][]int{
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9},
    }
    fmt.Println(matrix2)
}

슬라이스와 메모리

1. 슬라이스 공유 문제

func sliceSharing() {
    original := []int{1, 2, 3, 4, 5}
    
    // 슬라이싱은 같은 배열 공유
    slice1 := original[1:4]
    slice2 := original[2:5]
    
    slice1[1] = 999
    
    fmt.Println("original:", original) // [1 2 999 4 5]
    fmt.Println("slice1:", slice1)     // [2 999 4]
    fmt.Println("slice2:", slice2)     // [999 4 5]
}

2. 메모리 누수 방지

func avoidMemoryLeak() {
    // ❌ 메모리 누수 가능
    func() []int {
        data := make([]int, 1000000)
        // ... 데이터 초기화 ...
        
        // 작은 부분만 반환하지만 전체 배열이 메모리에 유지됨
        return data[:10]
    }
    
    // ✅ 복사하여 반환
    func() []int {
        data := make([]int, 1000000)
        // ... 데이터 초기화 ...
        
        result := make([]int, 10)
        copy(result, data[:10])
        return result // data는 GC 대상이 됨
    }
}

3. 용량 사전 할당

func preAllocate() {
    // ❌ 비효율적: 매번 재할당
    var s []int
    for i := 0; i < 1000; i++ {
        s = append(s, i)
    }
    
    // ✅ 효율적: 용량 사전 할당
    s = make([]int, 0, 1000)
    for i := 0; i < 1000; i++ {
        s = append(s, i)
    }
}

실전 활용 패턴

1. 스택 구현

type Stack struct {
    items []int
}

func (s *Stack) Push(item int) {
    s.items = append(s.items, item)
}

func (s *Stack) Pop() (int, bool) {
    if len(s.items) == 0 {
        return 0, false
    }
    index := len(s.items) - 1
    item := s.items[index]
    s.items = s.items[:index]
    return item, true
}

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

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

2. 큐 구현

type Queue struct {
    items []int
}

func (q *Queue) Enqueue(item int) {
    q.items = append(q.items, item)
}

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

func (q *Queue) Front() (int, bool) {
    if len(q.items) == 0 {
        return 0, false
    }
    return q.items[0], true
}

3. 중복 제거

func removeDuplicates(slice []int) []int {
    seen := make(map[int]bool)
    result := []int{}
    
    for _, value := range slice {
        if !seen[value] {
            seen[value] = true
            result = append(result, value)
        }
    }
    
    return result
}

func main() {
    numbers := []int{1, 2, 2, 3, 3, 3, 4, 5, 5}
    unique := removeDuplicates(numbers)
    fmt.Println(unique) // [1 2 3 4 5]
}

4. 병합 정렬된 슬라이스

func mergeSorted(a, b []int) []int {
    result := make([]int, 0, len(a)+len(b))
    i, j := 0, 0
    
    for i < len(a) && j < len(b) {
        if a[i] < b[j] {
            result = append(result, a[i])
            i++
        } else {
            result = append(result, b[j])
            j++
        }
    }
    
    // 남은 요소 추가
    result = append(result, a[i:]...)
    result = append(result, b[j:]...)
    
    return result
}

func main() {
    a := []int{1, 3, 5, 7}
    b := []int{2, 4, 6, 8}
    merged := mergeSorted(a, b)
    fmt.Println(merged) // [1 2 3 4 5 6 7 8]
}

5. 슬라이스 청크 분할

func chunkSlice(slice []int, chunkSize int) [][]int {
    var chunks [][]int
    
    for i := 0; i < len(slice); i += chunkSize {
        end := i + chunkSize
        if end > len(slice) {
            end = len(slice)
        }
        chunks = append(chunks, slice[i:end])
    }
    
    return chunks
}

func main() {
    numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    chunks := chunkSlice(numbers, 3)
    fmt.Println(chunks) // [[1 2 3] [4 5 6] [7 8 9] [10]]
}

6. 제네릭 슬라이스 함수 (Go 1.18+)

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

// Filter 함수
func Filter[T any](slice []T, fn func(T) bool) []T {
    result := []T{}
    for _, v := range slice {
        if fn(v) {
            result = append(result, v)
        }
    }
    return result
}

// Reduce 함수
func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
    acc := initial
    for _, v := range slice {
        acc = fn(acc, v)
    }
    return acc
}

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

성능 최적화

1. 용량 사전 할당 벤치마크

import "testing"

func BenchmarkAppendWithoutCap(b *testing.B) {
    for i := 0; i < b.N; i++ {
        var s []int
        for j := 0; j < 1000; j++ {
            s = append(s, j)
        }
    }
}

func BenchmarkAppendWithCap(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := make([]int, 0, 1000)
        for j := 0; j < 1000; j++ {
            s = append(s, j)
        }
    }
}

// 결과: WithCap이 약 10배 빠름

2. In-place 연산

// ❌ 새 슬라이스 생성 (메모리 할당)
func transform1(s []int) []int {
    result := make([]int, len(s))
    for i, v := range s {
        result[i] = v * 2
    }
    return result
}

// ✅ In-place 수정 (메모리 효율적)
func transform2(s []int) {
    for i := range s {
        s[i] *= 2
    }
}

일반적인 실수

1. Append 반환값 무시

func mistake1() {
    s := []int{1, 2, 3}
    
    // ❌ 반환값 무시
    append(s, 4)
    fmt.Println(s) // [1 2 3] (변경 안 됨)
    
    // ✅ 반환값 할당
    s = append(s, 4)
    fmt.Println(s) // [1 2 3 4]
}

2. 슬라이스 순회 중 수정

func mistake2() {
    s := []int{1, 2, 3, 4, 5}
    
    // ❌ 순회 중 슬라이스 확장 (예측 불가능)
    for _, v := range s {
        if v%2 == 0 {
            s = append(s, v*10)
        }
    }
    // range는 초기 슬라이스의 복사본을 순회
    
    // ✅ 인덱스 기반 순회
    for i := 0; i < len(s); i++ {
        if s[i]%2 == 0 {
            s = append(s, s[i]*10)
        }
    }
}

3. 슬라이스 복사 시 참조 공유

func mistake3() {
    s1 := []int{1, 2, 3}
    
    // ❌ 참조만 복사
    s2 := s1
    s2[0] = 999
    fmt.Println(s1) // [999 2 3]
    
    // ✅ 값 복사
    s3 := make([]int, len(s1))
    copy(s3, s1)
    s3[0] = 100
    fmt.Println(s1) // [999 2 3]
    fmt.Println(s3) // [100 2 3]
}

4. nil vs 빈 슬라이스 혼동

func mistake4() {
    var s1 []int      // nil 슬라이스
    s2 := []int{}     // 빈 슬라이스
    s3 := make([]int, 0)
    
    fmt.Println(s1 == nil) // true
    fmt.Println(s2 == nil) // false
    fmt.Println(s3 == nil) // false
    
    // 대부분의 경우 동일하게 동작
    fmt.Println(len(s1), len(s2), len(s3)) // 0 0 0
    
    // JSON 직렬화 차이
    // s1 -> null
    // s2, s3 -> []
}

5. 슬라이스 비교

func mistake5() {
    s1 := []int{1, 2, 3}
    s2 := []int{1, 2, 3}
    
    // ❌ 컴파일 에러: 슬라이스는 == 비교 불가
    // fmt.Println(s1 == s2)
    
    // ✅ nil 비교만 가능
    fmt.Println(s1 == nil) // false
    
    // ✅ 수동 비교
    equal := len(s1) == len(s2)
    if equal {
        for i := range s1 {
            if s1[i] != s2[i] {
                equal = false
                break
            }
        }
    }
    fmt.Println(equal) // true
    
    // ✅ reflect 사용 (성능 저하)
    fmt.Println(reflect.DeepEqual(s1, s2)) // true
}

nil 슬라이스 처리

func nilSliceHandling() {
    var s []int
    
    // ✅ nil 슬라이스도 안전하게 사용 가능
    fmt.Println(len(s))    // 0
    fmt.Println(cap(s))    // 0
    
    for range s {
        // 실행 안 됨
    }
    
    s = append(s, 1, 2, 3)
    fmt.Println(s) // [1 2 3]
    
    // ❌ nil 체크 불필요
    // if s != nil {
    //     for _, v := range s { ... }
    // }
    
    // ✅ 길이 체크로 충분
    if len(s) > 0 {
        // ...
    }
}

정리

  • 슬라이스는 동적 배열의 뷰 (포인터, 길이, 용량)
  • 참조 타입: 슬라이싱 시 내부 배열 공유
  • nil 슬라이스: 안전하게 사용 가능, nil 체크 불필요
  • make([]T, len, cap)로 용량 사전 할당
  • append() 반환값을 반드시 할당
  • copy()로 깊은 복사
  • 슬라이스는 == 비교 불가 (nil 비교만 가능)
  • 용량 초과 시 자동 확장 (일반적으로 2배)
  • Full slice expression으로 용량 제한 가능
  • 큰 데이터 처리 시 용량 사전 할당으로 성능 향상
  • In-place 연산으로 메모리 효율성 개선
  • 슬라이스 공유 문제 주의 (메모리 누수 가능)
  • 배열보다 슬라이스 사용 권장