[Go] 슬라이스 본문
Slice란?
슬라이스는 Go 언어에서 제공하는 자료구조 중 하나로, 동적으로 크기를 증가시키는 동적 배열이다. 또한, 슬라이싱 기능을 이용해 배열의 일부를 나타내는 슬라이스를 만들 수 있다.
Slice 선언
🚀{}
를 이용한 초기화
var slice []int
일반적인 배열과 달리 []
내부에 배열의 개수를 적지 않고 선언한다. 별도로 슬라이스의 크기를 지정해주지 않으면, 길이가 0인 슬라이스가 생성된다.
var slice []int
if len(slice) == 0 {
fmt.Println("slice is Empty")
}
// slice is Empty
슬라이스 초기화 시점에 슬라이스 내부에 포함되는 값들을 미리 설정하기 위해서는 배열처럼 {}
를 이용하여 요소값을 지정해야 한다.
var slice1 = []int {1, 2, 3} // [1, 2, 3]
var slice2 = []int {1, 5:2, 10:3} // [1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 3]
slice1
은 1, 2, 3을 값으로 갖는 슬라이스가 되며, slice2
는 인덱스 1은 1, 인덱스 5는 2, 인덱스 10은 3을 각각 값으로 갖고 나머지는 0인 슬라이스로 초기화 된다.
🚀make()
를 이용한 초기화
내장 함수 make()
를 사용하는 방법도 있다.
함수의 첫 번째 인자로 생성하고자 하는 슬라이스의 타입을 입력하고, 두 번째 인자로 해당 슬라이스의 길이를 작성한다.
var slice = make([]int, 3) // [0, 0, 0]
슬라이스에 포함된 값은 int
타입의 기본 값인 0으로 초기화된다.
Slice 요소 추가
➕ append
슬라이스의 요소를 조회하거나 순회하는 방식은 일반 배열과 동일하지만, 값을 추가하는 방식에서 일반적인 배열과 차이점이 존재한다.
일반적인 배열은 초기화 시점에서 길이가 정해지면 길이를 늘릴 수 없지만, 슬라이스는 요소를 추가하는 것으로 길이를 늘릴 수 있다.
요소를 추가하기 위해서는 내장 함수인 append()
를 사용하면 된다. 다른 언어에서 사용되는 것과 동일하게 슬라이스의 맨 뒤에 요소를 추가한다.
이때 기존의 슬라이스 값을 변경하는 것이 아닌 인수를 추가한 슬라이스를 반환하기 때문에 append()
의 결과를 대입할 값이 필요하다.
var slice = []int {1, 2, 3}
sliceForAppend := append(slice, 4)
fmt.Println(slice) // output > 1, 2, 3
fmt.Println(sliceForAppend) // output > 1, 2, 3, 4
append()
를 사용해 slice
에 4를 추가해 반환한 값을 sliceForAppend
에 대입한다.
append()
를 사용하면 여러 개의 값을 한 번에 추가할 수도 있다.
var slice = []int {1, 2, 3}
sliceForAppend := append(slice, 4, 5, 6)
fmt.Println(slice) // output > 1, 2, 3
fmt.Println(sliceForAppend) // output > 1, 2, 3, 4, 5, 6
Slice 동작 원리
슬라이스는 내장 타입으로 내부 구현이 감춰져 있지만, SliceHeader
구조체를 사용해 내부 구현을 살펴볼 수 있다. 슬라이스 내부 정의는 다음과 같다.
type SliceHeader struct {
Data uintptr // 실제 배열을 가리키는 포인터
Len int // 요소 개수
Cap int // 실제 배열의 길이
}
슬라이스 구조체는 배열을 가리키는 포인터와 요소 개수를 나타내는 len
, 전체 배열 길이를 나타내는 cap
필드로 구성되어 있다.
슬라이스가 실제 배열을 가리키는 포인터를 가지고 있기 때문에 쉽게 크기가 다른 배열을 가리키도록 변경할 수 있고, 슬라이스 변수 대입 시 배열에 비해서 사용되는 메모리나 속도에 이점이 있다.
var slice := make([]int, 3)
var slice2 := make([]int, 3, 5)
slice
는 len
이 3이고 cap
또한 3이다. slice2
는 len
이 3이고 cap
은 5다. 그림을 통해 살펴보면 다음과 같다.
🚀Slice와 배열의 동작 차이
슬라이스의 내부 구현이 배열과 다르기 때문에 동작 또한 배열과 다르다. 예제를 통해 차이를 살펴보자.
package main
import "fmt"
func changeArray(array2 [5]int) {
array2[2] = 200
}
func changeSlice(slice2 []int) {
slice2[2] = 200
}
func main() {
array := [5]int{1, 2, 3, 4, 5}
slice := []int{1, 2, 3, 4, 5}
changeArray(array)
changeSlice(slice)
fmt.Println("array:", array) // array: [1, 2, 3, 4, 5]
fmt.Println("slice:", slice) // slice: [1, 2, 200, 4, 5]
}
배열의 값은 변경되지 않는 반면, 슬라이스는 값이 변경되는 것을 확인할 수 있다.
Go언어에서 모든 값의 대입은 복사로 일어난다. 함수에 인수로 전달될 때나 다른 변수에 대입할 때나 값의 이동은 복사로 일어난다.
복사는 타입의 값이 복사되는데, 포인터의 경우 포인터의 값인 메모리 주소가 복사되고 구조체가 복사될 때는 구조체의 모든 필드가 복사된다.
changeArray()
함수에 인자로 들어간 array
는 int
타입의 요소 다섯 개를 가지고 있다. 즉, array
의 크기는 40byte인 것을 알 수 있다. (8 x 5 = 40) changeArray()
함수가 동작했을 때 array
의 값이 array2
로 복사된다.
array
와 array2
는 별도의 메모리 공간을 가지는 아예 별개의 배열인 것이다. 그렇기 때문에, array2
의 요소값을 변경해도 array
의 요소값에는 아무런 영향이 없는 것이다.
🚀append()
를 사용했을 때 발생할 수 있는 문제 (1)
append()
함수가 호출되면 먼저 슬라이스에 값을 추가할 수 있는 빈 공간이 있는지 확인한다. 남은 빈 공간은 cap
에서 len
을 뺀 값이다.
남은 빈 공간의 개수가 추가하는 값의 개수보다 크거나 같은 경우 배열 뒷부분에 값을 추가한 뒤 len
값을 증가시킨다.
다음 코드를 수행했을 때 slice1
과 slice2
에서 발생하는 문제점과 원인에 대해서 알아보자.
package main
import "fmt"
func main() {
slice1 := make(int[], 3, 5)
slice2 := append(slice1, 4, 5)
fmt.Println("slice1:", slice1, len(slice1), cap(slice1))
// slice1: [0, 0, 0], 3, 5
fmt.Println("slice2:", slice2, len(slice2), cap(slice2))
// slice2: [0, 0, 0, 4, 5], 5, 5
slice1[1] = 100
fmt.Println("slice1:", slice1, len(slice1), cap(slice1))
// slice1: [0, 100, 0], 3, 5
fmt.Println("slice2:", slice2, len(slice2), cap(slice2))
// slice2: [0, 100, 0, 4, 5], 5, 5
slice1 := append(slice1, 500)
fmt.Println("slice1:", slice1, len(slice1), cap(slice1))
// slice1: [0, 100, 0, 500], 4, 5
fmt.Println("slice2:", slice2, len(slice2), cap(slice2))
// slice2: [0, 100, 0, 500, 5], 5, 5
}
출력된 결과를 보면 알 수 있다시피 slice1
에 값을 변경, 추가했을 때 slice2
에도 해당 변화가 영향을 미치는 것을 확인할 수 있다.
이는 두 슬라이스 모두 동일한 배열의 주소를 포인터로 가지고 있기 때문이다. 즉, 다음의 그림과 같은 상황이 발생한다.
슬라이스가 배열의 주소를 포인터로 가지고 있고, append()
사용 시 해당 주소값을 가지고 있는 포인터가 복사된다는 것을 알고있지 못한다면 이렇듯 예기치 못한 문제에 마주칠 수 있다.
🚀append()
를 사용했을 때 발생할 수 있는 문제 (2)
append()
함수가 호출되면 빈 공간이 있는지 확인한 후 빈 공간이 충분하지 않다면 통상적으로 기존 배열의 2배 크기의 새로운 배열을 마련한다. 그리고 새로운 배열에 기존 배열의 모든 요소를 복사한 뒤, 맨 뒤에 새 값을 추가한다.
이 과정에서 cap
은 새로운 배열의 길이가 되고, len
은 기존 길이에 추가한 개수만큼 더한 값이 되며, 포인터는 새로운 배열을 가리키는 슬라이스 구조체를 반환한다.
package main
import "fmt"
func main() {
slice1 := []int{1, 2, 3}
slice2 := append(slice1, 4, 5)
fmt.Println("slice1:", slice1, len(slice1), cap(slice1))
// slice1: [1, 2, 3], 3, 3
fmt.Println("slice2:", slice2, len(slice2), cap(slice2))
// slice2: [1, 2, 3, 4, 5], 5, 6
slice[1] = 100
fmt.Println("slice1:", slice1, len(slice1), cap(slice1))
// slice1: [1, 100, 3], 3, 3
fmt.Println("slice2:", slice2, len(slice2), cap(slice2))
// slice2: [1, 2, 3, 4, 5], 5, 6
slice1 := append(slice1, 500)
fmt.Println("slice1:", slice1, len(slice1), cap(slice1))
// slice1: [1, 100, 3, 500], 4, 6
fmt.Println("slice2:", slice2, len(slice2), cap(slice2))
// slice2: [1, 2, 3, 4, 5], 5, 6
}
append()
함수로 인해 slice1
과 slice2
가 서로 다른 배열의 주소를 포인터로 가지게 됐기 때문에 slice1
의 변화에도 slice2
는 영향을 받지 않는다.
Slicing
슬라이싱은 배열의 일부를 집어내는 기능을 의미한다. 슬라이싱을 사용하면 그 결과로 슬라이스를 반환한다.
array[startIndex:endIndex] // 시작 인덱스 ~ 끝 인덱스 - 1
슬라이싱을 통해 반환된 슬라이스는 포인터 값으로 메모리 주소를 갖기 때문에 배열의 중간을 가리킬 수도 있다.
array := [5]int{1, 2, 3, 4, 5}
slice := array[1:2]
fmt.Println("array:", array)
fmt.Println("slice:", slice, len(slice), cap(slice))
array[1] = 100
fmt.Println("array:", array)
fmt.Println("slice:", slice, len(slice), cap(slice))
slice := append(slice, 500)
fmt.Println("array:", array)
fmt.Println("slice:", slice, len(slice), cap(slice))
위의 코드를 그림으로 표현하면 아래의 그림과 같다. 배열을 슬라이싱을 통해 슬라이싱을 통해 가져오면 cap
은 배열의 총 길이에서 시작 인덱스를 뺀 4로, len
은 슬라이스에 포함된 요소의 수만큼 각각 할당된다.
시작 인덱스와 끝 인덱스 2개만 사용할 때 cap
은 배열의 전체 길이에서 시작 인덱스를 뺀 값이 된다. 인덱스를 3개 사용하여 cap
까지 조절이 가능하다.
array[startIndex:endIndex:maxIndex]
시작 인덱스부터 끝 인덱스 하나 전까지 추출하고 최대 인덱스까지만 배열을 사용한다는 의미다. 즉, 슬라이스의 cap
값은 최대 인덱스 - 시작인덱스가 된다. 다음 예시를 살펴보자.
slice1 := []int {1, 2, 3, 4, 5}
slice2 := slice1[1:3:4]
인덱스 1부터 2까지 값을 추출한 slice2
는 [2, 3]
이 되고, cap
은 최대 인덱스가 4이므로 1이 된다.
이 경우 slice1[1:3:4]
의 의미는 slice1
이 가리키는 전체 배열 길이를 전부 사용하는 것이 아니라 인덱스 4까지만 배열을 사용하겠다는 의미가 된다.
즉, 배열의 두 번째부터 네 번째까지만 배열을 사용하게 된다.
Slice 복제
슬라이스가 서로 동일한 배열을 가르키기 때문에 발생하는 문제를 해결하기 위해, 두 슬라이스가 서로 다른 배열을 가르키게 만드는 방법 또한 존재한다.
바로 슬라이스를 복제하는 것이다. slice1
이 가리키는 배열과 똑같은 배열을 복제한 뒤 slice2
가 가리키게 한다면 서로 다른 배열을 가리키기 때문에 slice1
의 변화에도 slice2
는 영향을 받지 않을 것이다. 반대 또한 마찬가지다.
반복문을 사용하는 방법과 append()
함수를 사용하는 방법 또한 존재하지만, 내장 함수인 copy()
를 활용하는 방법을 알아보자.
func copy(dst, src []Type) int
copy()
함수의 첫 번째 인수로 복사한 결과를 저장하는 슬라이스 변수를 넣고, 두 번째 인수로 복사 대상이 되는 슬라이스 변수를 넣는다. 반환값은 실제로 복사된 요소의 개수다.
Slice 요소 삭제
슬라이스의 중간에 위치한 요소를 삭제하는 방법에 대해서도 알아보자. 아래의 그림처럼 중간의 요소를 삭제하고 다른 요소의 위치를 옮겨야 한다고 가정해보자.
append()
함수를 사용해 위의 그림을 구현해보자.
slice = append(slice[:idx], slice[idx+1:])
굉장히 간단하다. 한 번 각 인덱스가 어떤 역할을 하는지 분석해보자.
slice[:idx]
는 슬라이스의 처음부터 끝 요소의 바로 앞에 위치한 요소까지 집어낸 슬라이스다. 지우고자 하는 인덱스의 요소는 포함되지 않는다. 위의 그림에서 지워야 하는 값은 3이기 때문에 slice[:idx
는 [1, 2]
가 된다.
slice[idx+1:]
은 idx
의 하나 뒤의 값부터 끝까지 집어낸 슬라이스다. slice[idx+1:]
는 [4, 5, 6]
이 될 것이다.
마지막으로 append()
를 통해 [1, 2]
와 [4, 5, 6]
을 이어주면 [1, 2, 4, 5, 6]
이 된다.
Slice 요소 추가
슬라이스 중간에 요소를 추가하는 방법에 대해서 알아보자. 전개되는 상황은 아래의 그림과 동일하다.
append()
함수를 사용하여 위의 그림을 구현하는 코드는 아래와 같다.
slice = append(slice, 0)
copy(slice[idx+1:], slice[idx:])
slice[idx] = 100
append(slice, 0)
으로 슬라이스 맨 뒤에 요소를 추가한다. 내장 함수 copy()
를 사용해 슬라이스 값을 복사한다. 이때, 첫 번째 인수는 복사하는 위치, 두 번째 인수는 복사하려는 대상이다.
slice[idx + 1:]
즉 삽입하려는 위치 하나 다음부터 끝까지는 slice[idx:]
위치부터 복사한다. 그렇게되면 한 칸씩 밀려서 복사가 이루어지게 된다. 이후 idx
의 위치에 100을 삽입한다.
Slice 정렬
int
타입의 슬라이스를 정렬하는 방법은 매우 간단하다.
s := []int{5, 2, 3, 4, 1}
sort.Ints(s)
fmt.Println(s) // [1, 2, 3, 4, 5]
그렇다면 구조체 타입의 슬라이스를 정렬하려면 어떻게 해야 할까? 구조체 슬라이스를 정렬하기 위해서는 Sort()
함수 사용에 필요한 Len()
, Less()
, Swap()
세 메소드를 구현해야 한다.
Golang에서의 메소드 구현 방식에 대한 자세한 설명은 다음 포스팅에서 다뤄보기로 하고 우선 코드를 살펴보자.
package main
import (
"fmt"
"sort"
)
type Student struct {
Name string
Age int
}
type Students []Student
func (s Students) Len() int { return len(s) }
func (s Students) Less(i, j int) bool { return s[i].Age < s[j].Age }
func (s Students) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func main {
s := []Student {
{"화랑", 31}, {"백두산", 52}, {"류", 42},
{"켄", 37}, {"송하나", 17}
}
sort.Sort(Students(s))
fmt.Println(s)
}
// [{"송하나", 17}, {"화랑", 31}, {"켄", 37}, {"류", 42}, {"백두산", 52}]
[]Student
의 별칭 타입인 Students
를 만들고 Len()
, Less()
, Swap()
메소드를 구현하는 것으로 sort.Interface
를 사용할 수 있게 되었다.
Less()
메소드를 통해 Student
구조체의 Age
필드를 통해 값을 비교하도록 선언했다.
'Dev' 카테고리의 다른 글
[Go] 인터페이스 (3) | 2024.11.10 |
---|---|
[Go] 메소드 (2) | 2024.11.09 |
그림으로 이해하는 객체 지향 설계 5원칙 [SOLID] (2) | 2024.07.20 |
CPU-Scheduling (2) | 2024.04.07 |
RabbitMQ (0) | 2024.03.31 |