Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

Contents

소개

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 := []byte{'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o'}
b := s[1:4]
fmt.Println(string(b))      // ell
fmt.Println(string(s[1:4])) // ell
s[1] = 'k'
fmt.Println(string(b))      // kll
하면 된다. 슬라이스는 복사가 아닌, 레퍼런스다. 따라서 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"

슬라이싱은 배열의 복사가 아니다. 원본 배열은 배열을 더 이상 참조하지 않을 때까지 메모리에 자리를 차지한다. 따라서 원본 배열의 일부만 참조하는데, 배열 전체를 메모리에 유지하는 문제가 생길 수 있다.

아래 예제를 보자.
var digitRegexp = regexp.MustCompile("[0-9]+")

func FindDigits(filename string) []byte {
	b, _ := ioutil.ReadFile(filename)
	return digitRegexp.Find(b)
}
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
}

참고