처음 Go언어의 Array와 Slice를 접했을 때, C에서 생각하던 개념과 다르다는 것을 확인할 수 있었다.
Go에서는 Array와 Slice를 어떻게 구성하고 있는지 이해해보도록 하자.
Arrays
array는 기본적으로 변수의 이름, array의 크기와 array를 구성하는 요소의 type을 정의를 통해 확인할 수 있다.
var arr [4]int
// arr 변수이름, 배열의 크기, element type
기본적으로 array는 초기화를 별도로 해줄 필요는 없고, zero value로 알아서 채워주게 된다.
var arr [4]int
if arr[0] == 0 {
fmt.Println("이미 난 초기화가 되어있어")
}
Go의 array는 값(value)이며, array의 변수는 배열 전체를 가리킨다. C언어의 케이스는 배열의 첫번째 주소를 가리키는 포인터지만 Go는 그렇지 않다는 것을 주의하자! 이 뜻은 array 값을 다른 변수에 assign하거나 파라미터로 넘겨줄 때, 해당 array의 값을 복사(copy)한다는 것을 의미한다. (만약 값의 copy를 원치 않는다면 배열의 포인터를 넘겨주도록하자. 하지만 배열의 포인터가 배열은 아니라는 것을 이해하자.) 따라서 Array는 인덱스로 이루어진 필드를 가진 struct의 일종으로 생각하면 될 것 같다.
아래는 [4]int를 메모리 형태로 표현한 그림. 4개의 integer가 차례대로 이어져있다.
numArr := [4]int{2, 4, 8, 10} //numArr은 array
numArrP := &numArr // numArrP는 numArr을 가리키는 포인터
numArr2 := numArr //numArr의 값을 copy하여 numArr2에 할당!
numArr[0] = 3
if(numArr[0] != numArr2[0]){
fmt.Println("거봐 다르자나")
}
if(numArr[0] == numArrP[0]){
fmt.Println("이건 포인터라서 그래!")
}
Slice
array는 length가 고정적이므로 자유도가 떨어지는 단점이 있음. 따라서 Slice를 이용하는 경우가 실제로는 많다.
기본적으로 slice는 길이를 명시 하지 않는다.
sliceTest := []int {1, 2, 3, 4}
slice는 위와 같은 방식으로 생서이 될 수 있지만 make라고 하는 built-in function을 이용할 수도 있다.
func make([]T, len, cap) []T
T는 element의 type이고, len는 array의 길이, cap은 capacity로 길이는 아니지만 slice가 참조할 수 있는 범위를 말한다. 이때 cap은 optional!
보통 아래와 같이 선언.
var s []byte
s = make([]byte, 5)
//s = make([]byte, 5, 5) 와 동일!
//s = []byte{0,0,0,0,0} 와 동일!
fmt.Println(len(s), cap(s))
//len과 cap은 위와 같이 확인할 수 있음.
length와 capacity와의 관계
slice의 zero value는 nil이고 len과 cap은 0을 return하게 된다.
슬라이스는 위 예제와 같이 array를 allocation하는 방법도 있지만, array나 slice를 slicing 해서도 만들 수 있다.
var arr = [4]int{1, 2, 3, 4}
//slice 참조 생성!
a := arr[1:3] //2,3
//a := arr[:3] //1,2,3
Slice 내부 동작
slice는 array segment의 descriptor라고 볼 수 있다. 구성은 array의 주소를 담고있는 pointer, segment의 길이, capacity (해당 segment의 최대 길이)로 되어있다.
x := [5]byte{0, 0, 0, 0, 0}
s := x[:] //x배열을 참조하는 slice 생성
위 코드의 slice는 아래와 같이 표현할 수 있다.
길이는 이 slice가 참조하는 배열의 크기를 말하고, capacity는 참조를 시작하는 배열의 위치부터 배열의 끝까지의 길이를 말한다.
s = s[2:4] //len = 2, cap = 3
새로운 slice가 copy될 때는 최초에 point하고있는 array를 참조하는 slice가 새로 만들어진다. 따라서 새로 만들어진 slice에서 data를 변경하면 마찬가지로 original array에서도 데이터가 변경된다.
위의 코드에서 s의 len은 cap보다 적은 상태로 slicing되었는데, 아래 코드와 같이 새롭게 slicing하면 len을 cap의 크기 만큼 확장할 수 있다.
s = s[:cap(s)] // len = 3, cap = 3
* 참고로 slice는 capacity 보다 크게 확장 불가하고 0인덱스 이전의 값을 참조할 수 없다.(참조하고 있는 array index 이전의 index)
Slice 크기 키우기
slice의 capacity를 늘리려면 새롭고 더 큰 slice를 만들고, 원래의 데이터를 모두 새로운 slice로 복제해야한다. (이 과정은 다른 언어의 dynamic array가 백그라운드에서 동작하는 방식이다.)
t := make([]byte, len(s), (cap(s)+1)*2) // +1 cap(s) == 0 인 경우를 위해.
for i := range s {
t[i] = s[i]
}
s = t
위 코드에서 데이터를 for문으로 copy하고 있는데, 이 과정을 내장된 copy 함수를 활용할 수 있다.
func copy(dst, src []T) int
copy함수는 서로 길이가 다른 두 slice의 데이터 복제가 가능하고, 길이가 더 짧은 array까지 데이터를 복제하게 된다.
아래는 copy를 사용한 코드이다.
t := make([]byte, len(s), (cap(s)+1)*2) // +1 cap(s) == 0 인 경우를 위해.
copy(t, s)
s = t
일반적으로 slice의 맨 마지막에 데이터를 붙이는 케이스인데, 아래 AppendByte 함수를 통해 byte를 append하고, 필요하다면 slice를 키워서(capacity가 충분치 않을 경우) update된 slice를 리턴하도록 한다.
func AppendByte(slice []byte, data ...byte) []byte {
m := len(slice)
n := m + len(data)
if n > cap(slice) { // 필요한 공간이 cap보다 크다면
// 미래를 위해 필요한 크기의 2배만큼 공간을 할당한다.
newSlice := make([]byte, (n+1)*2)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:n]
copy(slice[m:n], data)
return slice
}
위와 같이 AppendByte를 만들어 써도 되지만, Go에서 제공하는 append를 사용하면 더욱 편리하다..!
func append(s []T, x ...T) []T
아래와 같이 사용해볼 수 있다.
a := make([]int, 1)
// a == []int{0}
a = append(a, 1, 2, 3)
// a == []int{0, 1, 2, 3}
아래 예제에서 ...은 해당 slice를 각각의 element로 펼치는 역할을 한다.
a := []string{"Yoon", "Moon"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // "append(a, b[0], b[1], b[2])"와 동일.
// a == []string{"Yoon", "Moon", "George", "Ringo", "Pete"}
그리고 Slice의 zero value는 nil이고 length가 0인 slice와 같이 행동하므로 slice를 선언하고, for loop에서 append하는 방식으로 활용할 수 있다.
// Filter는 fn()를 만족하는 element만
//포함하는 slice를 return합니다.
func Filter(s []int, fn func(int) bool) []int {
var p []int // == nil
for _, v := range s {
if fn(v) {
p = append(p, v) //필요하다면 capacity를 늘려서 append해준다.
}
}
return p
}
조금 더 알아보기
앞서 이야기했던 것 처럼, re-slice를 할 때는 내부에 참조중인 array는 복사되지 않는다. 그러면 re-slice를 통해 original array의 극히 일부만 참조를 하고 나머지 대부분은 참조하지 않는다면 메모리가 낭비될 것이다.
아래의 예시를 보도록 하자.
var digitRegexp = regexp.MustCompile("[0-9]+")
func FindDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
//모든 파일 내용이 들어있는 array를 참조하는 slice가 반환된다.
return digitRegexp.Find(b)
}
파일을 읽은 다음, 정규식에 해당하는 데이터 일부를 참조하는 slice를 return하고 있다. 이때, 전체 파일의 일부를 참조중인 slice로 인해 slice가 참조를 끊지 않는한 계속해서 메모리에 올라와 있어야하는 문제가 생긴다.
따라서 아래와 같이 수정해볼 수 있다.
func CopyDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
b = digitRegexp.Find(b)
c := make([]byte, len(b))
copy(c, b)//필요한 내용만 copy수행
return c
}
이전 코드와 달라진 것은 기존 file 내용이 담긴 array자체를 참조하는 것이 아닌 필요한 부분에 대해서만 copy를 해서 그 내용에 대한 slice를 return 해준다는 것이다. 이때 file내용이 담긴 array는 참조가 사라지기 때문에 garbage collector에 의해 메모리에서 할당 해제될 것이고, copy된 array만 살아남게 될 것이다.
'Language > Go' 카테고리의 다른 글
[기초] Go 기초 정리 - 4 (Method) (0) | 2022.07.05 |
---|---|
구조체를 포함하는 구조체 (0) | 2022.07.04 |
[기초] Go 기초 정리 - 3 (Pointer, Struct, Array, Slice, Range, Map, Function value, Function Closure) (0) | 2022.06.26 |
[기초] Go 기초 정리 - 2 (For, If, Switch, Defer) (0) | 2022.06.15 |
[기초] Go 기초 정리 - 1 (import, 함수, 변수, 상수, 자료형) (0) | 2022.06.15 |