The Ultimate Go Study Guide의 내용을 참고하여 작성했습니다.
슬라이스는 var 변수명 []타입
의 형태로 선언하며, make(타입, 길이, 용량)
라는 내장 함수를 이용하여 생성합니다. make
함수는 슬라이스, 맵, 채널 객체를 생성할때 사용합니다. 아래 예제는 5개의 요소를 갖는 슬라이스를 생성합니다. 예제 처럼 용량을 생략할 경우 용량이 길이와 같은 값을 갖습니다.
fruits := make([]string, 5) fruits[0] = "Apple" fruits[1] = "Orange" fruits[2] = "Banana" fruits[3] = "Grape" fruits[4] = "Plum"
슬라이스의 첫 번째 워드는 5개의 문자열 배열을 갖는 배열을 가리키고, 두 번째 워드는 길이, 세 번째 워드는 크기를 나타냅니다. 이처럼 슬라이스는 배열을 감싸고 있습니다.
길이는 포인터 위치에서 접근해서 사용할 수 있는 배열 요소의 수를 의미하고, 용량는 포인터 위치에서 추가할 수 있는 배열 요소의 최대수를 의미합니다. 슬라이스의 사용 방법은 배열과 비슷하며, 요소 할당 비용도 배열과 동일합니다. 한 가지 다른 점은 []string
처럼 대괄호 안에 값이 없다는 것입니다. 실제로 크기가 고정되어 있는 경우에만 배열을 사용하고 대부분의 경우에는 슬라이스를 사용하게됩니다.
5개의 요소를 갖고 용량이 8개인 슬라이스를 만든 후 슬라이스의 길이, 크기, 각 요소의 주소와 값을 출력하는 예제입니다. len
함수는 슬라이스의 길이를, cap
함수는 슬라이스의 크기를 알려줍니다.
fruits := make([]string, 5, 8) fruits[0] = "Apple" fruits[1] = "Orange" fruits[2] = "Banana" fruits[3] = "Grape" fruits[4] = "Plum" fmt.Printf("Length=%d Capacity=%d\n", len(slice), cap(slice)) <출력> Length=5 Capacity=8
슬라이스는 함수에 전달할 때 참조값을 전달한다는 설명을 찾을 수 있습니다. 좀 더 정확한 설명이 필요합니다. Go언어에서는 함수를 호출할 때 매개변수 값을 복사해서 함수 내부에 전달합니다. 그래서 함수에서 전달된 매개변수의 원본 값을 변경할 수 없습니다. 함수에서 원본 값을 변경하려면 포인터를 사용해야합니다.
그럼 왜 슬라이스는 참조를 전달한다고 설명하는 걸까요? 이유는 슬라이스의 값은 3워드로 구성된 배열의 참조, 길이, 크기 값을 의미하기 때문입니다. 데이터에 해당하는 배열의 메모리 주소도 그대로 복사되므로 데이터에 대한 참조를 복사하게됩니다. 아래 예제를 살펴보면 함수 안/밖에서 출력한 슬라이스의 메모리 주소가 서로 다르지만, 배열의 0번째 요소의 메모리 주소는 같은 것을 확인할 수 있습니다. 이를 통해 슬라이스의 값은 복사되어 전달되지만, 배열의 메모리 주소도 복사되므로 배열의 각 요소에 대한 참조가 전달되는 것과 같은 결과를 얻게된다는 것을 알 수 있습니다.
func main() { fruits := [3]{} fruits[0] = "Apple" fruits[1] = "Orange" fruits[2] = "Banana" fmt.Printf("1: %p\t%p\n", &fruits, &fruits[0]) printAddress(fruits) } var printAddress = func(slice []string) { fmt.Printf("2: %p\t%p\n", &slice, &slice[0]) } <출력> 1: 0xc000198000 0xc00019a000 2: 0xc000198018 0xc00019a000
아래 예제는 위 예제에서 슬라이스를 배열로 대체하여 출력한 결과입니다. 배열은 함수에 매개변수로 전달할때 배열의 포인터가 가르키는 값을 복사하여 전달하는데요, 결과를 확인해보면 배열의 메모리 주소 뿐만 아니가 배열 요소의 메모리 주소 까지도 다르게 출력되는 것을 확인할 수 있습니다. 배열은 배열의 데이터까지도 복사하여 전달하므로 너무 큰 배열을 함수에 전달할 경우 주의를 할 필요가 있습니다.
func main() { fruits := [3]string{} fruits[0] = "Apple" fruits[1] = "Orange" fruits[2] = "Banana" fmt.Printf("1: %p\t%p\n", &fruits, &fruits[0]) printAddress(fruits) } var printAddress = func(slice [3]string) { fmt.Printf("2: %p\t%p\n", &slice, &slice[0]) } <출력> 1: 0xc00007c180 0xc00007c180 2: 0xc00007c1b0 0xc00007c1b0
슬라이스는 내부적으로 배열을 사용하기 때문에 각 요소의 주소 값들이 순차적으로 증가하는 것을 확인할 수 있습니다.
fruits := make([]string, 5, 8) fruits[0] = "Apple" fruits[1] = "Orange" fruits[2] = "Banana" fruits[3] = "Grape" fruits[4] = "Plum" printAddress(fruits) var printAddress = func(slice []string) { for i := range slice { fmt.Printf("[%d] %p %s\n", i, &slice[i], slice[i]) } } <출력> [0] 0xc0000be000 Apple [1] 0xc0000be010 Orange [2] 0xc0000be020 Banana [3] 0xc0000be030 Grape [4] 0xc0000be040 Plum
문자열로 nil
슬라이스를 선언하면 슬라이스의 값은 제로값으로 설정됩니다. 첫번째 워드는 nil
, 두 번째와 세 번째 워드는 0
으로 표현되는 워드 자료구조입니다.
var data []string // <1> data := []string{} // <2>
위 <1>, <2>
두 가지 표현은 같은 표현일까요? 정답은 다릅니다. <2>
와 같이 빈 리터럴 표현식을 사용할 경우 데이터는 제로값으로 설정되지 않고, 빈 배열에 대한 포인터를 갖습니다. <1>, <2>
를 marshal
함수에 넘기면 전혀 다른 결과를 얻게됩니다.
var data []string fmt.Printf("data is nil? %t\ncapacity is %d\n", data == nil, cap(data)) b, _ := json.Marshal(data) fmt.Printf("json representation is %s\n", string(b)) <출력> data is nil? true capacity is 0 json representation is null --- data := []string{} fmt.Printf("data is nil? %t\ncapacity is %d\n", data == nil, cap(data)) b, _ := json.Marshal(data) fmt.Printf("json representation is %s\n", string(b)) <출력> data is nil? false capacity is 0 json representation is []
리슬라이스Re-slice는 원본 슬라이스에 대한 뷰View를 생성합니다. 원본 슬라이스에 대한 복사본을 만드는 것이 아닌 부분에 대한 참조만을 갖습니다. 리슬라이스는 [시작 인덱스:(시작 인덱스 + 길이)]
의 형태로 생성합니다.
아래 예제를 보면 fruits
를 리슬라이싱하여 1~3번 인덱스를 참조하는 punnet
슬라이스를 생성합니다. punnet
슬라이스의 요소를 변경했을때 fruits
슬라이스의 값도 함께 변경되는 것을 통해서 같은 배열을 참조하고 있음을 알 수 있습니다.
func main() { fruits := make([]string, 5) fruits[0] = "Apple" fruits[1] = "Orange" fruits[2] = "Banana" fruits[3] = "Grape" fruits[4] = "Plum" punnet := fruits[1:4] punnet[2] = "Strawberry" fmt.Println(fruits) fmt.Println("---") fmt.Println(punnet) fmt.Println("---") inspectSlice(fruits) fmt.Println("---") inspectSlice(punnet) } func inspectSlice(slice []string) { fmt.Printf("Length[%d] Capacity[%d]\n", len(slice), cap(slice)) for i := range slice { fmt.Printf("[%d] %p %s\n", i, &slice[i], slice[i]) } } <출력> [Apple Orange Banana Strawberry Plum] --- [Orange Banana Strawberry] --- Length[5] Capacity[5] [0] 0xc000118000 Apple [1] 0xc000118010 Orange [2] 0xc000118020 Banana [3] 0xc000118030 Strawberry [4] 0xc000118040 Plum --- Length[3] Capacity[4] [0] 0xc000118010 Orange [1] 0xc000118020 Banana [2] 0xc000118030 Strawberry
리슬라이스하여 사용할 경우 주의해야 할 점은 원본 슬라이스의 배열을 참조하고 있기 때문에 원본 슬라이스가 더 이상 사용하지 않게 돼도 배열이 메모리에 남게 된다는 점입니다.
내장 함수인 copy
함수를 이용하여 슬라이스를 복사할 수 있습니다. 슬라이스를 복사할 때눈 원본의 요소들을 담을 수 있는 새 슬라이스를 만들고 copy
함수를 이용하여 값을 복사합니다.
아래 예제에서 fruits
를 리슬라이스하여 punnet
에 복사합니다. 이후 punnet
에 특정 요소를 다른 값으로 변경하여도 fruits
에는 영향이 없는 것을 확인할 수 있습니다.
fruits := make([]string, 5) fruits[0] = "Apple" fruits[1] = "Orange" fruits[2] = "Banana" fruits[3] = "Grape" fruits[4] = "Plum" punnet := make([]string, 3) copy(punnet, fruits[1:4]) punnet[2] = "Strawberry" fmt.Println(fruits) fmt.Println(punnet) <출력> [Apple Orange Banana Grape Plum] [Orange Banana Strawberry]
슬라이스는 append
함수로 요소를 추가할 경우 크기가 부족하면 동적으로 슬라이스의 크기를 증가 시킵니다. 이를 슬라이스의 크기 증가Slice capacity growth라고 이야기 합니다.
자세히 이야기 해보면 내장 함수 append
는 슬라이스가 참조하는 배열의 끝에 하나 이상의 요소를 추가합니다. 슬라이스의 크기가 충분한 경우에는 새 요소를 위해서 리슬라이스됩니다. 반대로 크기가 충분하지 않은 경우 더 큰 배열을 생성하여 값을 복사한 후 새 배열을 참조한 후 슬라이스를 반환합니다.
슬라이스의 이런 특성 때문에 특정 요소를 변수로 참조할 경우 주의가 필요합니다. 아래 예제에서 numSlice
는 크기가 증가하면서 내부적으로 참조하는 배열이 변경됩니다. 즉, willBeOutdate
변수는 이전 배열의 요소를 참조하고 있어서 numSlice
의 요소 값을 변경해도 willBeOutdate
의 값은 변경되지 않습니다. 이는 프로그래머가 의도한 결과가 아닐 것이므로 주의해야 합니다.
numSlice := make([]int, 7) for i := 0; i < 7; i++ { numSlice[i] = i * 100 } // numSlice의 1번째 요소를 참조 willBeOutdate := &numSlice[1] fmt.Printf("Before append:\t%p\t%p\n", &numSlice, &numSlice[0]) // append 함수 호출 시 슬라이스의 값이 복사되어 전달되므로 반환값을 사용해야 함 numSlice = append(numSlice, 800) numSlice[1]++ fmt.Printf("After append:\t%p\t%p\n", &numSlice, &numSlice[0]) fmt.Println("willBeOutdate:", *willBeOutdate, "numSlice[1]:", numSlice[1]) <출력> Before append: 0xc000198000 0xc00018c040 After append: 0xc000198000 0xc0001a0000 elementAtOne: 100 numSlice[1]: 101
추가로 src/runtime/slice.go
에 슬라이스의 크기 증가 시 증가되는 크기에 대한 알고리즘이 구현되어 있습니다.
<1>
<2>
<3>
newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap // <1> } else { if old.len < 1024 { newcap = doublecap // <2> } else { // Check 0 < newcap to detect overflow // and prevent an infinite loop. for 0 < newcap && newcap < cap { newcap += newcap / 4 // <3> } // Set newcap to the requested cap when // the newcap calculation overflowed. if newcap <= 0 { newcap = cap } } }
길이와 크키가 같은 슬라이스에 요소를 추가하여 크기가 어떻게 증가하는지 확인하는 예제입니다.
func appendAndPrintCapacity(slice []int) { newSlice := append(slice, 1) fmt.Println(cap(newSlice)) } appendAndPrintCapacity(make([]int, 128)) // 128 + 128 appendAndPrintCapacity(make([]int, 256)) // 256 + 256 appendAndPrintCapacity(make([]int, 512)) // 1024 + 1024 appendAndPrintCapacity(make([]int, 1024)) // 1024 + 1024 / 4 appendAndPrintCapacity(make([]int, 2048)) // 2048 + 2048 / 4 <출력> 256 512 1024 1280 2560
Go언어에서는 슬라이스의 요소를 삭제하는 문법을 별도로 제공하지 않습니다. 아래와 같이 append
함수와 리슬라이스를 이용하여 슬라이스의 요소를 삭제할 수 있습니다.
fruits := make([]string, 5) fruits[0] = "Apple" fruits[1] = "Orange" fruits[2] = "Banana" fruits[3] = "Grape" fruits[4] = "Plum" fmt.Printf("Length[%d] Capacity[%d] %v\n", len(fruits), cap(fruits), fruits) // Banana를 삭제 fruits = append(fruits[:2], fruits[3:]...) fmt.Printf("Length[%d] Capacity[%d] %v\n", len(fruits), cap(fruits), fruits) <출력> Length[5] Capacity[5] [Apple Orange Banana Grape Plum] Length[4] Capacity[5] [Apple Orange Grape Plum]
만약 크기도 줄이고 싶다면 아래와 같이 합니다. 배열 복사가 이루어지므로 잘 고민해보고 사용해야합니다.
func main() { fruits := make([]string, 5) fruits[0] = "Apple" fruits[1] = "Orange" fruits[2] = "Banana" fruits[3] = "Grape" fruits[4] = "Plum" fmt.Printf("Length[%d] Capacity[%d] %v\n", len(fruits), cap(fruits), fruits) // Banana를 삭제 fruits = deleteAt(2, fruits) fmt.Printf("Length[%d] Capacity[%d] %v\n", len(fruits), cap(fruits), fruits) } func deleteAt(index int, slice []string) []string { slice = append(slice[:index], slice[index+1:]...) newSlice := make([]string, len(slice)) copy(newSlice, slice) return newSlice } <출력> Length[5] Capacity[5] [Apple Orange Banana Grape Plum] Length[4] Capacity[4] [Apple Orange Grape Plum]