슬라이스

Slice

슬라이스는 앞서 살펴본 배열과 연관성이 높은 또 하나의 타입이다. 슬라이스의 특징을 간략히 요약하면 다음과 같다.

  • 기반이 되는 배열, 문자열 또는 다른 슬라이스의 요소를 참조한다. 즉, 슬라이스 요소의 값을 바꾸면 기반 배열, 문자열 또는 슬라이스 요소의 값도 바뀐다.
  • 기반 배열, 문자열 또는 슬라이스의 일부만 참조할 수도 있다.
  • 요소의 개수를 동적으로 조절할 수 있다.

이상의 특징들에 의해 배열에 대한 참조 타입 또는 동적 배열처럼 사용할 수 있다.

슬라이스 타입

슬라이스 타입은 배열 타입에서 크기를 나타내는 ArrayLength만 생략해 쓴다.

SliceType = "[" "]" ElementType .

즉, 다음과 같이 int에 대한 슬라이스 변수를 만들 수 있다.

var slice []int

생성 및 초기화

슬라이스는 참조형이기 때문에 원본이 반드시 어딘가에 존재해야 한다. 이러한 특징은 포인터와 유사한데, 배열의 포인터와는 달리 슬라이스는 원본 배열의 일부만을 참조할 수도 있다는 특징이 있다.

리터럴 (literal) 초기화

배열처럼 슬라이스 변수를 선언과 동시에 LiteralValue를 이용해 초기화할 수도 있는데, 이는 슬라이스 자체가 값을 갖는다기 보다는 메모리 상 어딘가에 LiteralValue를 이용한 배열을 만들고 슬라이스가 그 배열을 참조한다고 보는 것이 타당하다.

slice := []int {1, 2, 3}

슬라이싱 (slicing)

슬라이싱은 배열 또는 다른 슬라이스의 전체 또는 일부를 참조해 새로운 슬라이스를 만드는 방법이다. simple, full 등 두 가지 방식을 사용할 수 있다.

Simple slice expression

a를 문자열, 배열, 배열의 포인터, 다른 슬라이스 중 하나라고 할 때 다음 방식으로 새로운 슬라이스를 만들 수 있다.

a[low:high]

low는 생성할 슬라이스가 포함할 a의 시작 인덱스이며, high는 포함하지 않는 종료 인덱스이다. 즉, low 이상, high 미만의 요소가 포함된 새로운 슬라이스가 만들어진다.

a := [5]int{1, 2, 3, 4, 5}
s := a[1:4]
// [2, 3, 4]

low와 high는 모두 생략할 수 있으며, 각각을 생략한 경우에는 다음과 같은 의미가 된다.

  • low 생략: 0
  • high 생략: len(a)

Full slice expression

위에서 언급한 것처럼 슬라이스는 요소의 개수를 동적으로 조절할 수 있다는 특징이 있다. 크기를 동적으로 조절할 수 있는 대부분의 자료구조가 취하는 공통적인 전략은 당장 필요한 것보다 약간의 여유를 갖고 메모리를 확보해 두는 것이다. 이렇게 하면 메모리 효율성 측면에서는 손해를 감수해야 하지만 자료를 추가할 때 여유분을 활용할 수 있기 때문에, 새로운 메모리를 할당하고 기존 자료들을 새로운 영역으로 복사, 기존 메모리를 해제하는 과정을 약간은 덜 수 있다. 이러한 취지로 슬라이스는 현재 참조하고 있는 요소의 개수와 확보하고 있는 용량(capacity)의 정보도 함께 가지고 있다. 현재 요소의 개수는 배열과 동일하게 len() 함수를 이용해 확인할 수 있으며 용량의 정보는 cap() 함수를 이용해 확인할 수 있다. 배열이나 문자열 등도 cap()을 이용해 용량의 정보를 확인할 수 있지만 이 타입들은 길이가 변하지 않기 때문에 요소의 길이와 용량의 정보가 같아 의미가 없다.

a := [5]int{1, 2, 3, 4, 5}
s := a[1:4]
fmt.Println(cap(s))
// 4

슬라이싱 표현에서 다음과 같이 세 번째 인자를 입력하면 새로 생성하는 슬라이스의 용량을 조절할 수 있다.

a[low:high:max]

이 표현은 simple 방식과는 달리 a가 문자열인 경우에는 사용할 수 없다.

max를 입력한 경우 슬라이스의 용량은 (max - low) 값을 갖는다.

Full 방식의 경우 low만 생략할 수 있으며, 생략한 경우 0을 입력한 것과 동일한 의미를 갖는다.

동적 할당

new() 함수를 이용해 포인터를 동적 할당하는 것처럼 슬라이스도 make() 함수를 이용해 동적 할당이 가능하다. make() 함수의 입출력 형태는 다음과 같다.

func make([]T, len, cap) []T

make()를 이용해 생성한 슬라이스 각 요소는 모두 zero value를 갖는다. 입력 인수 중 cap은 생략할 수 있고, 생략한 경우 len과 같은 값을 갖는다. 즉, 다음 두 슬라이스는 모두 길이와 용량이 4이며, 각 요소의 값이 0—int 타입의 zero value—이다.

var slice1, slice2 []int
slice1 = make([]int, 4, 4)
slice2 = make([]int, 4)

new()를 사용해서도 슬라이스를 동적으로 할당할 수 있다. 문법적으로 가능할 뿐 일반적으로 사용하지는 않으니 원리만 파악해 보자.

우선 new() 함수의 입출력 형태를 보면 다음과 같다.

func new(T) *T

슬라이스는 배열에 대한 참조이므로 new()를 통해 배열을 할당하고, 슬라이싱으로 슬라이스를 만드는 것이다.

new_slice := new([100]int)[0:100]

new([100]int)의 반환 타입은 *[100]int가 되므로, C 언어를 생각하면, 이를 배열의 포인터가 아닌 배열로 변경해야 해 *(new[100]int)로 써야하지만 그냥 배열의 포인터를 바로 써도 Go는 알아서 처리해 준다.

추가/삭제

append() 함수를 사용해 슬라이스에 요소를 추가할 수 있다.

func append(slice []T, elements ...T) []T

입력 슬라이스의 용량을 초과하는 경우에는 자동으로 확장된다.

s := make([]int, 4)
fmt.Println(len(s), cap(s))
// 4 4
s = append(s, 1)
fmt.Println(len(s), cap(s))
// 5 8

append() 함수의 추가할 요소를 가리키는 두 번째 인수는 가변 인수이기 때문에 원하는 개수의 요소를 한번에 추가할 수 있다. 추가하려는 요소가 슬라이스라면 다음과 같이 슬라이스 뒤에 ...을 붙여 가변 인수화 할 수 있다.

s = append(s, another_slice...)

슬라이스의 요소를 삭제하는 단일 함수는 제공되지 않는다. 하지만 다음과 같이 append() 함수와 슬라이싱을 이용해 간단히 구현할 수 있다.

func DelElement(slice []int, idx int) []int {
	slice = append(slice[:idx], slice[idx + 1]...)
	return slice
}