Table of Contents

Golang - 배열과 슬라이스 사용하기

배열


Go에서 배열은 거의 사용되지 않는다. 왜냐하면 Go에서는 타입을 컴파일 과정에서 반드시 해석되어야 하기 때문에 배열의 크기를 지정하기 위해 변수를 사용할 수 없고, 동일한 타입을 가진 다른 크기의 배열 간에 타입 변환을 시도할 수 없기 때문이다.
이런 제약들 때문에 정확히 배열의 크기를 아는 경우가 아니라면 배열을 사용하지 않는다.

배열 선언

배열의 모든 요소는 지정된 타입이어야 한다. 배열을 지정할 때 값을 초기화하지 않으면 정수 0으로 초기화된다.

1var x [3]int
2
3// 초기값 주기
4var x = [3]int{10, 20, 30}
5
6// 배열 리터럴로 배열 초기화
7var x = [...]int{10, 20, 30}

희소 배열 (대부분의 요소 값이 0으로 설정된 배열)을 만드려면 다음과 같이 사용한다.

1// [1, 0, 0, 4, 6, 0, 100, 15] 
2var x = [12]int{1, 3:4, 6, 6:100, 15}

다차원 배열 (배열의 요소로 다른 배열을 포함하는 배열)은 다음과 같이 선언한다.

1var x [2][3]int

배열 응용

==!= 를 사용하여 배열 간의 비교가 가능하다.

1var x = [...]int{10, 20, 30}
2var y = [3]int{10, 20, 30}
3fmt.Println(x == y)  // true

대부분의 언어와 마찬가지로 괄호를 이용해 값을 읽거나 쓸 수 있다.

1x[0] = 10
2fmt.Println(x[2])
3
4// 배열의 길이
5fmt.Println(len(x))

배열의 끝을 넘어서거나 음수의 인덱스를 사용하여 값을 읽거나 쓸 수 없다.

슬라이스


일련의 값을 가지고 있는 자료구조를 구성할 때는 배열의 제약을 제거한 슬라이스를 주로 사용한다. 슬라이스의 크기는 해당 타입의 일부가 아니기 때문이다. → 슬라이스 크기를 지정할 필요가 없다는 것을 뜻한다.

보통 슬라이스가 커질 일이 없다면 var 선언을 사용하고, 슬라이스가 커져야 하지만 어떤 값인지 정확히 알 수 없으면 make를 사용하여 선언한다.

슬라이스 선언

배열 선언과 비슷하지만 가장 큰 차이점은 슬라이스의 크기를 지정할 필요가 없다.

1var x = []int{10, 20, 30}
2
3// [1, 0, 0, 4, 6, 0, 100, 15] 
4var x = []int{1, 3:4, 6, 6:100, 15}
5
6// 다차원 슬라이스
7var x [][]int

배열과의 차이점

1var x []int  // nil
2
3fmt.Println(x == nil) // true

정수 슬라이스에서 값을 할당하지 않으면 x에는 nil이라는 제로 값으로 할당된다. Go에서 nil타입의 부재를 표현한 식별자이다. 슬라이스가 nil 이라는 것은 어떤 요소도 가지고 있지 않다는 뜻이다.

슬라이스는 비교가 불가능한 타입으로, 두 슬라이스를 비교하기 위해 ===! 를 사용하면 컴파일 오류가 발생한다. 슬라이스는 오로지 nil로만 비교가 가능하다.

슬라이스 응용

배열과 같이 크기를 넘어서거나 음수의 인덱스로는 읽기와 쓰기가 불가능하다.

1x[0] = 10
2fmt.Println(x[2])

슬라이스에서 len(), append() 같은 내장 함수들을 사용할 수 있다.

 1var x []int
 2
 3fmt.Println(len(x)) // 0
 4
 5x = append(x, 10)
 6fmt.Println(x)      // [10]
 7
 8x = append(x, 20, 30, 40)
 9fmt.Println(x)      // [10, 20, 30, 40]
10
11y := []int{20, 30, 40}
12x = append(x, y...)

슬라이스는 수용력(예약된 메모리 공간의 크기)을 가지는데, 슬라이스의 길이와 수용력이 같아진 시점에 값을 추가하면 append 는 더 큰 수용력을 가지는 슬라이스를 할당한다.

1var x []int
2fmt.Println(x, cap(x))  // 수용력 크기 출력

선택적으로 수용력을 지정하여 슬라이스를 만들 수 있다.

1x := make([]int, 5)  // 길이 5, 수용력 5를 가지는 정수 슬라이스 생성
2x := make([]int, 5, 10)  // 길이 5, 수용력 10을 가지는 정수 슬라이스 생성
3
4x = append(x, 10)    // [0, 0, 0, 0, 0, 10]

x[0] 부터 x[4]까지 접근 가능한 요소이며, 모두 0으로 초기화된다.

슬라이스 연산자를 사용하여 슬라이스에서 슬라이스를 만들 수 있다.

1a := []int{1, 2, 3, 4}   // [1, 2, 3, 4]
2b := x[:2]               // [1, 2]
3c := x[1:]               // [2, 3, 4]
4d := x[1:3]              // [2, 3]
5e := x[:]                // [1, 2, 3, 4]

슬라이스에서 슬라이스를 가져오면 데이터를 복사하지 않고 메모리를 공유하는 두 개의 변수를 가지게 된다. 이는 슬라이스의 요소를 변경하면 모든 슬라이스에 영향이 생긴다는 것을 의미한다.

슬라이스끼리 영향이 생기는 것을 막기 위해서는 하위 슬라이스에 append 를 사용하지 않거나 full slice expression을 사용해야 한다.

1x := make([]int, 0, 5)
2x = append(x, 1, 2, 3, 4)
3y := x[:2:2]
4z := x[2:4:4]
5
6// append 사용
7y = append(y, 30, 40, 50)  // x = [1, 2, 3, 4, 60]
8x = append(x, 60)          // y = [1, 2, 30, 40, 50]
9z = append(z, 70)          // z = [3, 4, 70]

완전한 슬라이스 연산(full slice expression)은 부모 슬라이스에서 파생된 하위 슬라이스에 얼마나 많은 메모리를 공유할 것인지 명확하게 해준다. 세번째 인자에는 부모 슬라이스의 수용력의 마지막 위치를 지정한다.
하위 슬라이스의 수용력은 세번째 인자 - 시작 오프셋 이 된다.

배열을 슬라이스로 만들수도 있다.

1x := [4]int{5, 6, 7, 8}     // [10, 6, 7, 8]
2y := x[:2]                  // [10, 6]
3z := x[2:]                  // [7, 8]
4x[0] = 10

배열을 슬라이스로 만들 때도 슬라이스에서 슬라이스를 만들 때와 마찬가지로 메모리를 서로 공유한다.

원본 슬라이스로부터 독립적인 슬라이스를 만드려면 copy를 사용할 수도 있다.

1x := []int{1, 2, 3, 4}
2y := make([]int, 4}
3num := copy(y, x)
4fmt.Println(y, num)    // [1, 2, 3, 4] 4
5
6a := []int{1, 2, 3, 4}
7b := make([]int, 2}
8num2 := copy(b, a)
9fmt.Println(b, num2)   // [1, 2] 2 

copy의 첫 번째 파라미터는 대상 슬라이스이고, 두 번째 파라미터는 원본 슬라이스이다. 길이를 기준으로 원본 슬라이스에서 대상 슬라이스로 값을 복사하고 실제 복사된 요소의 개수를 반환한다.