16. 슬라이스
16. 슬라이스
✅ 1. 슬라이스
1.1 선언
1
2
3
var slice []int
slice[1] = 30// panic: runtime error: index out of range [1] with length 0
- 슬라이스를 초기화하지 않으면 길이가 0인 슬라이스가 생성된다
- 길이를 초과하는 인덱스 접근은 런타임 에러가 발생한다
1.2 초기화
1
2
3
4
5
6
// 1. 중괄호 초기화
var slice1 = []int{1, 2, 3} // [...]int{1, 2, 3}과는 다르다 !!! (배열 vs 슬라이스)
var slice2 = []int(1, 5:2, 3:3) // [1 0 0 3 0 2]
// 2. make() 함수 사용
var slice = make([]int, 3)
- 중괄호를 이용하여 배열과 비슷하게 초기화할 수 있다.
- 인덱스를 직접 지정해서 초기화할 수 있다
make()
내장 함수를 이용해서 초기화할 수 있다- 만들고자 하는 타입과 길이를 인자로 받는다
1.3 순회
1
2
for i := 0; i < len(slice); i++ {}
for i, v := range slice {}
- 배열과 마찬가지로
len()
내장 함수 또는range
키워드를 이용해서 순회할 수 있다
1.4 요소 추가
- 배열은 길이를 늘릴 수 없고, 길이를 초과해서 요소를 넣을 수 없지만 슬라이스는 요소를 추가해 길이를 늘릴 수 있다
- 요소 추가에는
append()
내장 함수를 사용한다1
slice = append(slice, 1, 2, 3)
- 첫 번째 인자로 슬라이스를 받고, 그 뒤에 원하는 만큼의 값들을 적어주면 이후 값들이 슬라이스에 추가된 뒤 만들어진 새로운 슬라이스를 반환한다. 따라서 기존 슬라이스에 요소를 추가하고 싶을 땐
append()
결과를 기존 슬라이스에 대입해야 한다
✅ 2. 슬라이스 동작 원리
1
2
3
4
5
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
- 슬라이스는 배열을 가리키는 포인터와 요소 개수를 나타내는 len, 전체 배열 길이를 나타내는 cap 필드로 구성된 구조체다
- 슬라이스가 실제 배열을 가리키는 포인터를 가지고 있어서 다른 배열을 가리키도록 변경하기 쉽고, 변수 대입 시 배열에 비해 사용되는 메모리와 속도에 이점이 있다
2.1 make() 함수를 이용한 선언
1
2
var s1 = make([]int, 3) // Len: 3 , Cap: 3
var s2 = make([]int, 3, 5) // Len: 3 , Cap: 5
make()
함수는 Len만 지정할 수도 있고 Len과 Cap을 같이 정해줄 수도 있다
2.2 슬라이스와 배열의 동작 차이
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func changeArray(array [5]int) {
array[2] = 200
}
func changeSlice(slice []int) {
slice[2] = 200
}
func main() {
array := [5]int{1, 2, 3, 4, 5}
slice := []int{1, 2, 3, 4, 5}
changeArray(array)
changeSlice(slice)
fmt.Printf("array = %v\n", array) // array = [1 2 3 4 5]
fmt.Printf("slice = %v\n", slice) // slice = [1 2 200 4 5]
}
- Go 언어에서의 모든 값의 대입은 복사로 일어난다
changeArray()
- 함수에서 인자로 받는 배열은 함수 내부에서만 접근 가능하며 외부 배열과는 별개로 복사된 배열이므로 함수 외부 배열의 값을 변경하지 않는다
- 배열을 인자로 받아 총 40바이트(8x5) 값이 복사된다
changeSlice()
- 함수에서 인자로 받는 슬라이스 역시 슬라이스 복사가 일어나지만, 슬라이스는 배열 포인터를 필드로 가지는 구조체이다. 따라서 배열 포인터가 복사되어 함수 외부 슬라이스와 동일한 배열을 바라본다. 함수 내부에서 복사된 슬라이스와 함수 외부의 슬라이스가 가리키는 배열이 동일하므로 외부 슬라이스에서도 값 변경이 감지된다
- 슬라이스는 복사되어도 항상 24바이트(포인터, Len, Cap)가 복사된다
2.3 append()를 사용할 때 발생하는 예기치 못한 문제
append()
함수가 호출되면 실제 배열 길이 cap에서 현재 요소 개수 len을 뺀 값을 확인하여 슬라이스에 추가 공간이 있는지 확인한다1
남은 빈 공간 = cap - len
- 빈 공간이 있다면 값을 추가하고 len을 늘린 슬라이스 구조체를 반환한다
2.3.1 case 1
1
2
3
4
5
6
7
slice1 := make([]int, 3, 5)
slice2 := append(slice1, 4, 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에 남은 공간이 2칸이므로
append()
함수가 새로운 배열을 할당하지 않고 현재 배열에 값을 넣은 후 slice2에 대입한다 - 두 슬라이스가 동일한 배열포인터를 가지므로 slice1의 값을 변경하면 slice2의 값도 변경된다
2.3.2 case2
1
2
3
4
5
6
7
slice1 := make([]int, 3, 4)
slice2 := append(slice1, 4, 5)
slice1[1] = 100
fmt.Println("slice1 = ", slice1, len(slice1), cap(slice1)) // slice1 = [0 100 0] 3 4
fmt.Println("slice2 = ", slice2, len(slice2), cap(slice2)) // slice2 = [0 0 0 4 5] 5 8
- slice1에 남은 공간이 없어서
append()
함수가 새로운 배열을 만들고 기존 배열의 값을 모두 복사한 뒤 슬라이스를 생성해서 slice2에 대입한다- 일반적으로 cap이 부족한 경우 생성되는 배열의 cap은 기존 cap의 2배이다
- 두 슬라이스가 서로 다른 배열 포인터를 가지므로 슬라이스 값을 변경해도 다른 슬라이스에 영향을 주지 않는다
✅ 3. 슬라이싱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:2]
fmt.Println("slice:", slice, len(slice), cap(slice)) // slice: [2] 1 4
arr[1] = 100
fmt.Println("slice:", slice, len(slice), cap(slice)) // slice: [100] 1 4
slice = append(slice, 500)
fmt.Println("array:", arr) // array: [1 100 500 4 5]
fmt.Println("slice:", slice, len(slice), cap(slice)) // slice: [100 500] 2 4
fmt.Printf("%v\n", unsafe.SliceData(slice) == &arr[1]) // true
}
- 슬라이싱은 배열 또는 슬라이스 일부를 가리키는 슬라이스를 만드는 기능이다.
array[startIdx:endIdx]
와 같은 형태로 슬라이스를 초기화한다 - 시작 인덱스부터 끝 인덱스 전까지의 배열 요소를 가리키며 cap은 배열의 총 크기에서 시작 인덱스를 뺀 크기로 결정된다
1
2
3
4
5
6
slice1 := []int{1, 2, 3, 4, 5}
slice2 := slice1[:3]
slice3 := slice1[:]
fmt.Println(slice2) // [1 2 3]
fmt.Println(slice3) // [1 2 3 4 5]
- 슬라이싱 기준점이 양 끝단이라면 인덱스를 생략할 수 있다
✅ 4. 유용한 슬라이싱 기능 활용
4.1 슬라이스 복제
1
2
3
4
5
6
7
8
9
10
slice1 := []int{1, 2, 3, 4, 5}
// 1, append를 이용한 방식
slice2 := append([]int{}, slice1...)
// 2. copy를 이용한 방식
var slice3 = make([]int, 5)
_ = copy(slice3, slice1) // 복사된 요소 개수(두 인자의 요소 개수 중 더 적은 개수)를 반환한다
fmt.Println(slice2, slice3) // [1 2 3 4 5] [1 2 3 4 5]
append()
함수에 새로운 슬라이스를 넣고 복사할 슬라이스의 요소를 넣는다- 슬라이스를 새로 생성하므로 서로 다른 배열을 가리킨다
- 배열이나 슬라이스 뒤에
...
을 사용하면 모든 요솟값을 넣는다는 의미이다
copy()
함수의 첫 번째 인자는 복사한 결과를 저장할 변수, 두 번째 인자는 복사 대상이 되는 슬라이스 변수를 넣는다- 반환값은 실제로 복사된 요소의 개수로, 두 인자의 요소 개수 중 더 적은 개수까지만 복사된다
4.2 요소 삭제
1
2
3
4
5
6
slice1 := []int{1, 2, 3, 4, 5}
deletedIdx := 2
slice1 = append(slice1[:deletedIdx], slice1[deletedIdx+1:]...)
fmt.Println(slice1, cap(slice1)) // [1 2 4 5] 5
append()
함수로 삭제할 인덱스 전후의 값을 붙여서 삭제를 구현할 수 있다
4.3 요소 추가
1
2
3
4
5
6
7
8
9
10
11
12
slice1 := []int{1, 2, 3, 4, 5}
// 1. append를 이용한 방식
idx := 2
slice1 = append(slice1[:idx], append([]int{6}, slice1[idx:]...)...)
// 2. copy를 이용한 방식
slice1 = append(slice1, 0)
copy(slice1[idx+1:], slice1[idx:])
slice1[idx] = 6
fmt.Println(slice1)
append()
를 두 번 사용해서 요소를 추가한다copy()
함수를 이용하면 불필요한 메모리 사용 없이 요소를 추가할 수 있다
✅ 5. 슬라이스 정렬
5.1 int 슬라이스 정렬
1
2
s := []int{5, 4, 3, 2, 1}
sort.Ints(s)
- sort 패키지의
Ints()
,Float64s()
함수로 슬라이스를 정렬할 수 있다
5.2 구조체 슬라이스 정렬
- 구조체 슬라이스 정렬을 위해서는 구조체 슬라이스의
Len()
,Less()
,Swap()
메서드를 구현해야 한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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{
{Name: "Alex", Age: 27},
{Name: "Brooke", Age: 41},
{Name: "Riley", Age: 22},
{Name: "Mason", Age: 35},
{Name: "Hannah", Age: 19},
}
sort.Sort(Students(s))
fmt.Println(s) // [{Hannah 19} {Riley 22} {Alex 27} {Mason 35} {Brooke 41}]
}
[]Student
의 별칭 타입인Students
를 만들고 sort.Interface를 사용할 수 있도록 Len, Less, Swap 메소드를 구현한다Less()
메소드로 각 요소의 대소 기준을 정해준다sort.Sort()
인수로 sort.Interface 메소드를 포함한 Students 타입 슬라이스를 넣을 수 있다
sort.Ints()
함수는 sort 패키지가 제공하는 어댑터 타입 sort.IntSlice가 sort.Interface를 구현했기 때문에 개발자가 직접 구현할 필요 없이 정렬 가능하다
1
2
3
4
5
6
7
func Ints(x []int) { Sort(IntSlice(x)) }
type IntSlice []int
func (p IntSlice) Len() int { return len(p) }
func (p IntSlice) Less(i, j int) bool { return p[i] < p[j] }
func (p IntSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
This post is licensed under CC BY 4.0 by the author.