Go의 슬라이스
Go에서 배열의 경우 크기가 고정되어 있어 유연한 사용이 어렵다. 이를 해결하기 위해 슬라이스(slice)를 사용한다. 슬라이스는 배열의 일부를 가리키는 참조 타입이다.
슬라이스는 배열의 길이를 선언하지 않고 사용할 수 있으며, 동적으로 크기를 조절할 수 있다.
1var array [5]int = [5]int // 배열 선언
2var slice []int // 슬라이스 선언
3
만약 슬라이스를 초기화하지 않으면 길이가 0인 슬라이스가 생성된다.
초기화를 할 때는 배열처럼 {}를 사용하거나 make 함수를 사용한다.
1var slice1 []int // 길이가 0인 슬라이스
2var slice2 = []int{1, 2, 3}
3var slice3 = make([]int, 5) // 길이가 5인 슬라이스
4var slice4 = make([]int, 3, 5) // 길이가 3이고 용량이 5인 슬라이스
5
슬라이스에 접근할 때에는 배열과 동일하게 []를 사용한다.
1package main
2
3import "fmt"
4
5func main() {
6 var slice = []int{1, 2, 3, 4, 5}
7
8 fmt.Println(slice[0]) // 1
9 fmt.Println(slice[1]) // 2
10}
11
요소 추가
슬라이스에 요소를 추가할 때에는 append 함수를 사용한다. append 함수를 사용하면 기존 슬라이스의 맨 뒤에 요소를 추가해 만든 새로운 슬라이스를 반환한다.
1package main
2
3import "fmt"
4
5func main() {
6 var slice = []int{1, 2, 3}
7
8 slice = append(slice, 4)
9
10 fmt.Println(slice) // [1 2 3 4]
11
12 slice = append(slice, 5, 6, 7)
13
14 fmt.Println(slice) // [1 2 3 4 5 6 7]
15}
16
동작 원리
슬라이스는 SliceHeader 구조체로 구성되어 있다. 이 구조체는 슬라이스의 요소 개수Length, 길이Cap, 포인터Data를 가지고 있다. 슬라이스가 실제 배열을 가리키는 포인터를 소유함으로서 크기가 가변적인 배열을 구현할 수 있다.
그렇기에 다음 코드와 같이 배열과는 다른 동작을 보인다.
1package main
2
3import "fmt"
4
5func changeArray(array [5]int) {
6 array[0] = 10
7}
8
9func changeSlice(slice []int) {
10 slice[0] = 10
11}
12
13func main() {
14 var array = [5]int{1, 2, 3, 4, 5}
15 var slice = []int{1, 2, 3, 4, 5}
16
17 changeArray(array)
18 changeSlice(slice)
19
20 fmt.Println(array) // [1 2 3 4 5]
21 fmt.Println(slice) // [10 2 3 4 5]
22}
23
이는 함수의 인자로 넘어가면서 복사되는 값이 다르기에 발생하는 현상이다. 배열은 값이 복사되어 함수로 이동한다. 즉 복사되는 값의 크기는 배열의 크기와 같다. 하지만 슬라이스는 포인터를 포함한 구조체를 복사하므로 슬라이스의 크기와 상관없이 항상 고정된 크기의 메모리24 Byte를 사용한다.
Append 함수에서 발생할 수 있는 문제
append() 함수가 호출되면 우선 빈공간이 있는지 확인한다. 이는 cap - len과 동일하다. 만약 빈 공간이 추가할 값의 개수보다 크거나 같다면 배열 뒷부분에 값을 추가 후 len을 증가시킨다. 하지만 빈 공간이 부족하다면 새로운 배열을 생성한다. 일반적으로 새로운 배열의 크기는 기존 배열의 2배이다. 그리고 기존 배열의 값을 새로운 배열로 복사한 후 새로운 배열에 값을 추가한다. cap은 새로운 배열의 크기가 되고 len은 추가된 값의 개수가 된다. 이후 포인터는 새로운 배열을 가리키게 된다.
1package main
2
3import "fmt"
4
5func main() {
6 slice := make([]int, 3, 5) // 길이가 3이고 용량이 5인 슬라이스
7 firstSlicePointer := &slice[0] // 첫 번째 요소의 포인터
8
9 fmt.Println("slice:", slice, "firstSlicePointer:", firstSlicePointer) // slice: [0 0 0] firstSlicePointer: 0xc0000b6010
10
11 slice = append(slice, 1, 2) // 슬라이스에 1, 2 추가
12 firstSlicePointer = &slice[0] // 첫 번째 요소의 포인터
13
14 fmt.Println("slice:", slice, "firstSlicePointer:", firstSlicePointer) // slice: [0 0 0 1 2] firstSlicePointer: 0xc0000b6010 용량이 부족하지 않아 메모리 재할당이 일어나지 않음
15
16 slice = append(slice, 1) // 슬라이스에 1 추가
17 firstSlicePointer = &slice[0] // 첫 번째 요소의 포인터
18
19 fmt.Println("slice:", slice, "firstSlicePointer:", firstSlicePointer) // slice: [0 0 0 1 2 1] firstSlicePointer: 0x14000132000 용량이 부족하여 메모리 재할당이 일어남
20}
21
따라서 슬라이스를 사용할 때에는 append() 함수를 사용할 때 메모리 재할당이 일어날 수 있음을 염두해 두어야 한다. 메모리 재할당이 일어나면 기존 슬라이스의 포인터가 변경되므로 주의해야 한다.
슬라이싱
슬라이싱은 슬라이스나 배열에서 슬라이스를 추출하는 것을 말한다. 슬라이싱은 배열과 슬라이스 모두에서 사용할 수 있다.
1package main
2
3import "fmt"
4
5func main() {
6 var array = [5]int{1, 2, 3, 4, 5}
7 var slice = []int{1, 2, 3, 4, 5}
8
9 var slicedArray = array[1:3] // 배열의 1번째부터 3번째 요소까지 슬라이싱
10 var slicedSlice = slice[1:3] // 슬라이스의 1번째부터 3번째 요소까지 슬라이싱
11
12 fmt.Println(slicedArray) // [2 3]
13 fmt.Println(slicedSlice) // [2 3]
14}
15
이를 이용해서 슬라이스를 복사할 수도 있다.
1package main
2
3import "fmt"
4
5func main() {
6 var slice = []int{1, 2, 3, 4, 5}
7 var copiedSlice = make([]int, len(slice))
8
9 for i, v := range slice {
10 copiedSlice[i] = v
11 }
12
13 slice[0] = 10
14 fmt.Println(slice) // [10 2 3 4 5]
15 fmt.Println(copiedSlice) // [1 2 3 4 5]
16}
17
혹은 copy 함수를 사용할 수도 있다.
1package main
2
3import "fmt"
4
5func main() {
6 var slice = []int{1, 2, 3, 4, 5}
7 var copiedSlice = make([]int, len(slice))
8
9 copy(copiedSlice, slice)
10
11 slice[0] = 10
12 fmt.Println(slice) // [10 2 3 4 5]
13 fmt.Println(copiedSlice) // [1 2 3 4 5]
14}
15
요소 삭제
슬라이스에서 요소를 삭제할 때에는 append` 함수를 사용하면 삭제할 요소를 제외한 나머지 요소를 새로운 슬라이스로 복사하여 반환한다.
1package main
2
3import "fmt"
4
5func main() {
6 var slice = []int{1, 2, 3, 4, 5}
7
8 slice = append(slice[:2], slice[3:]...)
9
10 fmt.Println(slice) // [1 2 4 5]
11}
12
요소 삽입
슬라이스 중간에 요소를 삽입할 때에는 슬라이스의 맨 뒤에 요소를 추가한 후, 해당 요소를 원하는 위치로 이동시키면 된다.
1package main
2
3import "fmt"
4
5func main() {
6 var slice = []int{1, 2, 3, 4, 5}
7
8 slice = append(slice, 0)
9
10 copy(slice[3:], slice[2:])
11 slice[2] = 10
12
13 fmt.Println(slice) // [1 2 10 3 4 5]
14}
15