본문 바로가기
프로그래밍/React

Golang Slice(슬라이스) 동작원리와 사용법

by slowin 2024. 7. 31.

개요

해당 포스팅은 slices-intro(go.dev) 을 참고하여 작성되었습니다.

Golang 슬라이스(Slice)는 효율적이고 유연한 데이터 구조입니다. 배열과 유사하지만, 크기가 동적으로 조절될 수 있어 더 자유롭게 사용 할 수 있는 자료구조 or 구조체입니다.

배열

슬라이스 동작원리의 포스팅이지만, 슬라이스를 이해하려면 배열 부터 이해를 해야합니다. 간단하게 알아보고 넘어 가보겠습니다.

1. 배열의 기본 특성

  • 정의: Go에서 배열은 고정된 길이의 동일한 타입 요소들의 연속된 집합입니다.
  • 선언 예시: var a [5]int (5개의 정수를 저장할 수 있는 배열 선언)

2. 배열의 초기화와 접근

var a [5]int
a[0] = 1
i := a[0]
// a 출력 : [1 0 0 0 0]

 

  • 배열은 선언 시 모든 요소가 해당 타입의 제로값으로 초기화됩니다.
  • 인덱스를 통해 개별 요소에 접근할 수 있습니다 (0부터 시작).

3. 배열의 특징

  • 값 타입: 배열은 값 타입으로, 할당이나 함수 전달 시 전체가 복사됩니다.
  • 고정 크기: 배열의 크기는 타입의 일부로, [5]int와 [4]int는 서로 다른 타입입니다.
  • 메모리 표현: 배열은 메모리에 연속적으로 저장됩니다.

크기가 고정인 배열 메모리 표현 (출처:go.dev)

4. C 언어와의 차이점

  • Go의 배열 변수는 전체 배열을 나타내며, C와 달리 첫 번째 요소의 포인터가 아닙니다.
  • Go에서는 배열 전체가 값으로 취급되어 복사되거나 전달됩니다.

Slice(슬라이스)

1. Slice(슬라이스) 구조

Golang Slice(슬라이스)는 세 가지 요소로 구성된 작은 데이터 구조로 볼 수 있어요.

  • 길이 (Length)
  • 용량 (Capacity)
  • 배열의 요소를 가리키는 포인터
type sliceHeader struct {
    Length        int
    Capacity      int
    ZerothElement *byte
}

 

실제로 이 sliceHeader 구조체는 프로그래머에게 직접적으로 노출되지는 않아요. 이해를 돕기위한 개념적 모델로 이해해주시면 됩니다.

2. Slice(슬라이스) 내부

  • 슬라이스는 배열의 일부를 표현하는 데이터 구조입니다.
  • 배열에 대한 포인터, 세그먼트 길이, 용량(세그먼트의 최대 길이)으로 구성됩니다.

2.1 Slice(슬라이스)의 내부 구조

  • ptr (*Elem): 배열 요소를 가리키는 포인터
  • len (int): 슬라이스의 현재 길이
  • cap (int): 슬라이스의 최대 용량

슬라이스 구조 이미지(출처: go.dev)

2.2 슬라이스 생성 예시

s := make([]byte, 5)

 

위 코드를 생성한 메모리 구조 예시 이미지 입니다.

슬라이스 생성된 메모리 이미지(출처: go.dev)

2.3 슬라이싱

s = s[2:4]

 

  • 슬라이스는 원본 배열의 일부분을 참조합니다.
  • 슬라이싱 시 새로운 슬라이스 값이 생성되지만, 여전히 같은 기본 배열을 가리킵니다.
  • 길이(len)는 2, 용량(cap)은 3이 됩니다.

 

슬라이싱 후 원본 참조 (참고: go.dev)

원본을 참조하는 형태를 띄기 때문에, 슬라이싱된 슬라이스를 수정하면 원본도 같이 수정됩니다.

2. Slice(슬라이스) 생성 예시

slice := make([]int, 50, 100)

 

이렇게 생성된 슬라이스는 내부적으로 다음과 같은 구조를 생성된다고 보면 됩니다.

slice := sliceHeader{
    Length:        50,
    Capacity:      100,
    ZerothElement: &someArray[0],
}

3. Slice(슬라이스)의 동작 이해하기

  1. 길이(Length): 슬라이스에서 현재 사용 중인 요소의 수입니다. len() 함수로 확인할 수 있습니다.
  2. 용량(Capacity): 슬라이스가 사용할 수 있는 최대 요소의 수입니다. cap() 함수로 확인할 수 있습니다.
  3. 포인터: 실제 데이터가 저장된 배열의 첫 번째 요소를 가리킵니다.

Golang 슬라이스를 조작할 때 (예: 요소 추가, 제거), Go 런타임은 이 내부 구조를 업데이트합니다. 용량을 초과하면 새로운 배열이 할당되고 데이터가 복사됩니다.

실제 Go 코드에서는 이 내부 구조에 직접 접근할 수 없습니다. 슬라이스 조작은 언어에서 제공하는 내장 함수와 문법을 통해 이루어집니다.

 

예시 코드

// 길이가 5이고 용량이 10인 슬라이스 생성
slice := make([]int, 5, 10)
fmt.Println("초기 슬라이스:")
fmt.Printf("길이: %d, 용량: %d, 슬라이스: %v\n", len(slice), cap(slice), slice)
// 출력:
// 초기 슬라이스:
// 길이: 5, 용량: 10, 슬라이스: [0 0 0 0 0]

// 슬라이스에 요소 추가
slice = append(slice, 1, 2, 3)
fmt.Println("\n요소 추가 후:")
fmt.Printf("길이: %d, 용량: %d, 슬라이스: %v\n", len(slice), cap(slice), slice)
// 출력:
// 요소 추가 후:
// 길이: 8, 용량: 10, 슬라이스: [0 0 0 0 0 1 2 3]

// 용량을 초과하는 요소 추가
slice = append(slice, 4, 5, 6)
fmt.Println("\n용량 초과 후:")
fmt.Printf("길이: %d, 용량: %d, 슬라이스: %v\n", len(slice), cap(slice), slice)
// 출력:
// 용량 초과 후:
// 길이: 11, 용량: 20, 슬라이스: [0 0 0 0 0 1 2 3 4 5 6]

// 슬라이스의 일부분 선택
subSlice := slice[2:5]
fmt.Println("\n부분 슬라이스 (slice[2:5]):")
fmt.Printf("길이: %d, 용량: %d, 슬라이스: %v\n", len(subSlice), cap(subSlice), subSlice)
// 출력:
// 부분 슬라이스 (slice[2:5]):
// 길이: 3, 용량: 18, 슬라이스: [0 0 0]

4. Slice(슬라이스) 함수 인자로 전달

Golang 슬라이스를 함수에 전달할 때, 실제로 전달되는 것은 슬라이스 헤더의 복사본입니다.

type sliceHeader struct {
    Length        int
    Capacity      int
    ZerothElement *byte
}

이 헤더의 복사본이 함수에 전달되므로, 함수 내에서 슬라이스의 길이, 용량, 그리고 데이터에 대한 포인터를 조작할 수 있습니다.

 

함수에 전달해보도록 하겠습니다.

func modifySlice(s []int) {
    s[0] = 100
    s = append(s, 200)
}

func main() {
    slice := []int{1, 2, 3, 4, 5}
    fmt.Println("Before:", slice)
    modifySlice(slice)
    fmt.Println("After:", slice)
}

// 출력
// Before: [1 2 3 4 5]
// After: [100 2 3 4 5]

 

  • s[0] = 100는 원본 슬라이스를 수정합니다.
  • append(s, 200)는 함수 내부의 슬라이스 헤더만 변경하므로 main 함수의 slice에는 영향을 주지 않습니다.

특이한점은, s[0] = 100는 원본 슬라이스 가 수정되었다는 것입니다.

이유는, 슬라이스 헤더의 ZerothElement 포인터실제 데이터를 가리키기 때문입니다.

5. append 연산의 주의점

append 함수를 사용할 때는 주의가 필요합니다. append가 슬라이스의 용량을 초과하면 새로운 배열이 할당되고, 함수 내부의 슬라이스 헤더가 이 새 배열을 가리키게 됩니다. 하지만 이 변경은 함수 외부에 반영되지 않습니다.

6. 함수에서 Slice(슬라이스) 반환하기

슬라이스의 변경사항을 함수 외부에 반영하려면, 수정된 슬라이스를 반환하는 것이 좋을수 있어요.

func modifyAndReturn(s []int) []int {
    s[0] = 100
    return append(s, 200)
}

func main() {
    slice := []int{1, 2, 3, 4, 5}
    slice = modifyAndReturn(slice)
    fmt.Println("After:", slice)  // 출력: [100 2 3 4 5 200]
}

7. 포인터를 이용한 Slice(슬라이스) 전달

슬라이스 자체를 포인터로 전달하면, 함수 내에서 슬라이스 헤더 전체를 수정할 수 있어요.

func modifySlicePtr(s *[]int) {
    *s = append(*s, 200)
}

func main() {
    slice := []int{1, 2, 3, 4, 5}
    modifySlicePtr(&slice)
    fmt.Println("After:", slice)  // 출력: [1 2 3 4 5 200]
}

정리

Slice(슬라이스)를 함수 인자로 전달할 때 알아둬야 할 사항

  1. Slice(슬라이스) 헤더의 복사본이 전달되어요.
  2. Slice(슬라이스)의 요소를 수정하면 원본 슬라이스에도 반영되어요.
  3. append 연산 사용시 이슈사항을 알고있어야합니다.
  4. 필요한 경우 수정된 Slice(슬라이스)를 반환하거나 포인터를 사용해야해요.

시리즈

Golang Arrays(배열), Slice(슬라이스) - 선언, 초기화 방법

Golang Slice(슬라이스) 동작원리와 사용법

Golang Slice(슬라이스) : make, copy, append

Golang Slice(슬라이스) 요소 제거하기

 

참고

Arrays, slices (and strings): The mechanics of 'append'

 

'프로그래밍 > React' 카테고리의 다른 글

React Props,State  (0) 2024.07.29
React Virtual DOM  (0) 2024.07.29
React Composition  (0) 2024.07.29
React Function and Class Components  (0) 2024.07.29