Go의 slice를 이용하면 연속된 데이터 타입을 효과적으로 다룰 수 있다. 연속된 데이터를 다룬 다는 점에서 배열(array)와 비슷한 측면이 있다. 실제 배열 처럼 사용 할 수 있는데, 몇 가지 다른 점들이 있다. 이 문서에서 slice가 무엇인지 자세히 살펴본다.
배열
슬라이스(slice)타입은 Go의 내장타입으로 배열의 추상이다. 따라서 슬라이스를 이해하려면, 배열을 먼저 이해해야 한다.
배열은 특정 데이터 타입의 연속된 자료구조다. 예를들어 [4]int는 4개의 숫자를 저장하는 공간이다. 배열은 [4]int 나 [5]int 와 같이 고정이 된다. 배열에 저장된 데이터는 index를 이용해서 접근 할 수 있는데, s[n]과 같은 문법을 가진다. 인덱스 번호 n은 0부터 시작한다.
var a [4]int
a[0] = 1
i := a[0]
// i == 1
배열은 명시적으로 초기화 할 필요가 없다. 배열을 선언만 하고 값을 입력하지 않을 경우, 각 데이터의 타입에 따른 "zero value"가 설정된다. 예컨데, int는 0, float는 0.0, struct는 nil로 자동으로 초기화 된다. 위 코드에서 a[2] == 0 이다.
[4]int를 저장하기 위한 메모리 구조는 아래 그림처럼 묘사할 수 있다.
배열의 첫번째의 주소를 가리키는(포인트) C와는 달리 Go의 배열은 값이다. 배열변수는 전체 배열을 의미한다. 이는 배열을 다른 변수에 할당하면, 배열의 값 전체가 복사됨을 의미한다. 배열이 복사되는 걸 피하고 싶다면 배열의 포인터를 넘겨야 한다.
배열의 사용 방법
b := [2]string{"Penn", "Teller"}
컴파일 시간에 배열의 크기를 정하게 할 수도 있다.
b := [...]string{"Penn", "Teller"}
두 경우 모두다 [2]string 타입이다.
Slices
배열은 나름대로의 사용처를 가지고 있긴 하지만 유연성이 좀 떨어진다. Go 코드에서는 배열보다는 슬라이스를 주로 접하게 될 거다. 슬라이스는 배열처럼 사용할 수 있을 뿐만 아니라, 배열이 가지지 못한 편리함과 유연함도 얻을 수 있다.
슬라이스는 []T 형식으로 사용한다. T는 슬라이스를 이루는 값의 타입이다. 배열과 달리 슬라이스는 크기를 정할 필요가 없다.
슬라이스의 사용 예제다.
letters := []string{"a", "b", "c", "d"}
Go의 내장 함수인 make를 이용해서 슬라이스를 만들 수 있다.
func make([]T, len, cap) []T
T는 값의 데이터 타입이다. len은 슬라이크의 크기, cap은 용량으로 둘 다 옵션이다. cap이라고 하면 슬라이스의 총 크기라고 생각 할 수 있는데, 총크기가 아닌, 현재 참조하고 있는 값의 크기다.
make를 이용해서 슬라이스를 만들면 배열을 참조하는 슬라이스를 반환한다.
var s []byte
s = make([]byte, 5, 5)
// s == []byte{0,0,0,0}
용량(cap)를 생략하면, len을 용량으로 사용한다. 즉 위 코드는 s:= make([]byte, 5)와 같다. 값 전체를 참조한다는 의미다.
len(s) == 5
cap(s) == 5
슬라이스의 zero value는 nil이다. nil slice의 len과 cap은 0이다.
슬라이스는 이미 만들어져 있는 슬라이스와 배열을 슬라이싱(slicing) 할 수 있다. 슬라이싱에는 세미콜론(:)을 사용하고 인디게이터를 두는 것으로 범위를 설정할 수 있다. 예를 들어 b배열의 첫번째에서 4번째까지를 슬라이스로 만들고 싶다면
하면 된다. 슬라이스는 복사가 아닌, 레퍼런스다. 따라서 s와 b는 같은 공간을 사용한다.
인디게이터의 값이 0이거나 마지막(끝까지)일 경우 생략할 수 있다.
s := []byte{'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o'}
s[4:] // 4부터 마지막까지
s[:4] // 처음부터 4까지
s[:] // s 전체
Slice internals
슬라이스는 배열의 정보를 디스크립트한다. 배열의 포인터와 비슷하다.
변수를 s라고 하자. make([]byte, 5)로 만든 슬라이스의 구조는 아래와 같다.
https://docs.google.com/drawings/d/1XYobUxzLmE83gq2j7nNmEYMOntr8LLnv_g4r5dY0mHA/pub?w=585&h=247
Length는 슬라이스가 참조하는 값의 갯수다. 용량(Capacity)는 참조 중인 값의 인덱스 크기로 0부터 시작한다. 슬라이스는 len과 cap의 크기를 이용해서 인디게이터를 조작할 수 있다. 아래의 예제를 보자.
s = s[2:4]
그림에서 처럼 슬라이스는 인덱스를 이용해서 데이터를 참조 할 뿐, 데이터를 복사하지는 않는다. 따라서 원본 배열의 인덱스를 조작하면, 참조하는 슬라이스에도 동일한 영향을 미친다.
d := []byte{'r', 'o', 'a', 'd'}
e := d[2:]
// e == []byte{'a', 'd'}
e[1] = 'm'
// e == []byte{'a', 'm'}
// d == []byte{'r', 'o', 'a', 'd'}
슬라이스 크기 변경
슬라이스는 최초에 정해진 크기를 늘릴 수 없다. 크기를 늘리고 싶다면, 새로운 슬라이스를 만든 다음, 원본 슬라이스의 값들을 복사해야 한다.
s := make([]byte, 5)
fmt.Println(len(s), cap(s))
t := make([]byte, (cap(s)+1)*2)
for i := range s {
t[i] = s[i]
}
fmt.Println(len(t), cap(t))
위 코드는 루프를 돌고 있는데, 지저분하다. 내장 함수인 copy를 이용해서 깔금하게 복사하자.
func copy(dst, srt []T) int
copy 함수는 다른 크기의 슬라이스를 복사한다. 물론 목적지 슬라이스는 원본보다 더 커야 하겠다. 아래는 copy를 이용한 복사 예제다.
t := make([]byte, len(s), (cap(s)+1)*2)
copy(t,s)
s = t
슬라이드 끝에 값을 추가할 경우 슬라이스의 크기를 늘려야 한다. 아래는 슬라이스 끝에 값을 추가하는 함수다.
func AppendByte(slice []byte, data ...byte) []byte {
m := len(slice)
n := m + len(data)
if n > cap(slice) {
// 나중의 필요를 대비해서 2배로 늘린다.
newSlice := make([]byte, (n+1)*2)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:n]
copy(slice[m:n], data)
return slice
}
AppendByte는 슬라이스의 크기를 완전하게 제어한다. 위 코드에 재 할당 크기 제한만 넣어주면 거의 완전한 프로그램이 될거다.
그러나 항상 완전한 기능이 필요한 건아니다. 완전한 기능을 구현하려면 구현 비용이 추가된다. 예컨데, 완전한 랜덤함수를 뽑을 수 있으면 좋겠고 그래서 하드웨어로 부터 노이즈를 수집해서 랜덤 값을 뽑을 수 있는 함수들이 준비돼 있다. 하지만 느리고 많은 비용이 소모된다. 대부분의 경우에는 프로시저(procedure) 랜덤함수로 충분하다.
특별한 경우가 아니라면, Go의 내장함수인 append를 이용해도 충분하다.
a := make([]int, 1)
a = append(a, 1, 2, 3)
fmt.Println(cap(a)) // 4
a = append(a, 4)
fmt.Println(cap(a)) // 8
A possible "gotcha"
슬라이싱은 배열의 복사가 아니다. 원본 배열은 배열을 더 이상 참조하지 않을 때까지 메모리에 자리를 차지한다. 따라서 원본 배열의 일부만 참조하는데, 배열 전체를 메모리에 유지하는 문제가 생길 수 있다.
아래 예제를 보자.
ReadFile은 파일의 전체 내용을 슬라이스 타입으로 리턴한다. digitRegexp.Find는 패턴이 일치하는 슬라이스를 리턴한다. 여기에서 우리는 패턴매칭된 슬라이스만 필요하지만, 원본 슬라이스를 참조하고 이기 때문에 가비지 컬랙터는 원본 슬라이스를 지우지 않는다. 자원 낭비다.
이 문제는 필요한 슬라이스만 복사해서 반환하는 걸로 해결할 수 있다.
func CopyDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
b = digitRegexp.Find(b)
c := make([]byte, len(b))
copy(c,b)
return c
}
Contents
소개
배열
Slices
Slice internals
슬라이스 크기 변경
A possible "gotcha"
참고
Recent Posts
Archive Posts
Tags