Go언어에서 배열은 고정된 크기 안에 동일한 데이터를 연속으로 저장해 배열의 크기를 필요에 따라 동적으로 증가시키거나 부분 배열을 발췌하는 등의 기능을 가지고있지 않습니다. 그런데 'Slice(이하 슬라이스)'는 배열과 다르게 고정된 크기를 미리 지정하지 않고 이후에 필요에 따라 크기를 동적으로 변경할 수 있고, 부분 발췌가 가능합니다. 그리고 다차원 선언을 비롯한 배열의 모든 기능을 똑같이 구현할 수 있습니다. 따라서 슬라이스는 배열의 여러 제약점들을 넘어 여러 값을 다룰 때 개발자에게 주로 쓰입니다.
이러한 장점을 가진 (상대적으로)좋은 자료형인 슬라이스는 지금까지 배운 자료형과 내부적인 구조가 다르기때문에 선언 및 초기화를 할 때 주의해야합니다. 지금까지 배운 자료형의 선언 및 초기화방법과 비교해서 설명하겠습니다.
지금까지 배운 정수형(int, int32 ...등등)과 실수형(float32, float64...등등), 배열 등과 같은 자료형을 선언할때 "var 변수이름 자료형" 형식으로 선언했습니다. 예를들어 int형 변수 num을 선언한다면 var num int
와 같은 형태로 입력했습니다. 이 선언의 뜻은 "한개의 int형의 변수가 들어갈 메모리를 만들었다."는 말입니다. 그런데 Go언어에서는 아무런 값을 초기화 하지 않고 선언만 해도 정수나 실수형은 0, 문자열형은 ""(빈칸)이 자동 할당된다고 자료형 챕터에서 배웠습니다. 따라서 정확히 말한다면 선언과 동시에 자동 초기화도 되는 것입니다(자동으로 0, ""이 할당되기때문에).
다른 자료형과 마찬가지로, 배열도 크기를 지정하고 선언하기 때문에 명시한 개수만큼의 메모리를 만듭니다. 예를들어 아래 그림처럼 var arr2 [3]int
라고 입력하면 3개의 int타입의 변수가 들어갈 메모리를 만들고, 초기화하지 않았기 때문에 자동으로 0이 할당됩니다. 따라서 선언만 했을 뿐인데 len() 함수를 이용해 배열의 크기가 3이라는 것을 확인할 수 있습니다.
하지만 슬라이스를 위에서 설명한 것과 같은 방법으로 var a []int
와 같이 선언한다면 배열의 일부분을 가리키는 포인터를 만듭니다. 선언만 하고 초기화를 하지 않아서 슬라이스의 정보만 있는 배열만 생성되고, 실질적으로 어떠한 변수가 들어갈 공간(메모리)은 생성되지 않습니다. 그렇다면 '다른 자료형은 메모리를 만들고 자동으로 0이나 ""을 할당하는데, 왜 슬라이스는 만들지 않을까?'와 같은 궁금증이 생길 수 있습니다.
정말 간단합니다. 왜냐하면 슬라이스는 크기를 미리 지정하지 않기 때문에 컴퓨터가 어디서부터 어디까지 0이나 ""으로 채워야하는지 알 수 없기 때문입니다. 따라서 슬라이스의 초기 값을 지정하지 않고 선언만 한다면 'Nil silce'가 됩니다. 이것은 크기도 용량도 없는 상태를 의미합니다. 당연히 메모리를 만들지 않아서 존재하지도 않기 때문에 a[0] = 1
과 같이 값을 지정할 수 없습니다.
아래 그림을 봅시다.
기본적으로 슬라이스는 아무런 값도 초기화하지 않아도 배열의 위치를 가리키는 ptr과 배열의 길이인 len, 전체크기인 cap 메모리를 가지고 있습니다.
그렇기 때문에 슬라이스를 var a []int
와 같이 선언을 할 때는 주로 var a []int = []int{1, 2, 3, 4}
같이 선언과 동시에 값을 초기화할 때만 사용합니다. 이는 슬라이스를 선언함과 동시에 1, 2, 3, 4를 위한 메모리를 만든다는 뜻입니다. 이때부터 a[1] =18
과 같이 메모리에 저장돼있는 값을 바꿀 수 있고, 슬라이스의 길이와 용량을 확인하는 함수를 사용할 수 있습니다.
이러한 내부 구조를 이해한다면 슬라이스 복사를 쉽게 이해할 수 있습니다. 배열은 다른 배열의 값을 대입하면 값 자체가 대입됩니다. 하지만 슬라이스는 참조 타입이기 때문에 슬라이스를 복사해온다는 것은 사실 같은 주소를 참조한다는 것과 같은 말입니다. 예를 들어, 슬라이스는 다른 슬라이스를 부분 복사할 수 있는 기능이 있는데 슬라이스 a를 부분 복제하려고 하는 슬라이스 l은 l = a[2:5]
를 입력함으로써 슬라이스 a의 인덱스2 요소부터 4요소까지 참조합니다. 그렇기 때문에 슬라이스는 데이터의 복사 없이 데이터를 사용 할 수 있다는 장점이 있습니다. 이는 아래 그림과 같이 묘사 할 수 있습니다.
여기에 배열이 제공하지 않는 기능들을 사용하고 있으니 Go언어의 장점인 슬라이스를 잘 활용하는 것이 좋습니다.
참고로 슬라이스의 길이와 용량을 지정하지 않고 슬라이스를 선언만 해서 Nil slice만들면 nil과 비교할 수 있고 true을 반환합니다.
make() 함수를 이용한 슬라이스 선언
지금까지 슬라이스의 내부 구조에 대해 배우고 이에따른 선언과 초기화의 원리를 배웠습니다. 그렇다면 지금부터 슬라이스를 선언만 하면서 크기를 미리 지정할 수 있는 방법(즉, 값을 저장할 수 있는 메모리를 선언만 함으로써 생성)에 대해 배우겠습니다. 슬라이스를 생성하는 또 다른 방법으로는 Go언어의 내장 함수인 make() 함수를 이용한 선언입니다. 이 함수는 개발자가 슬라이스를 생성함과 동시에 슬라이스의 길이(len), 슬라이스의 용량(cap)을 저장할 수 있습니다. make() 함수는 "make(슬라이스 타입, 슬라이스 길이, 슬라이스의 용량)" 형태로 선언합니다.여기서 용량(Capacity)은 생략해서 선언할 수 있습니다. 용량을 생략한다면 슬라이스의 길이와 똑같은 값으로 선언됩니다. 이렇게 make() 함수를 이용해 선언한다면 비로소 모든 요소가 0인 슬라이스를 만들게 됩니다.
여기서 슬라이스의 길이와 용량의 개념이 헷갈릴 수 있습니다.
- 길이 : 초기화된 슬라이스의 요소 개수 즉, 슬라이스에 5개의 값이 초기화된다면 길이는 5가 됩니다. 그 후에 값을 추가하거나 삭제한다면 그만큼 길이가 바뀌게 됩니다. "len(컬렉션이름)"으로 길이를 알 수 있습니다.
- 용량 : 슬라이스는 배열의 길이가 동적으로 늘어날 수 있기 때문에 길이와 용량을 구분합니다. 예를 들어, 동호회에서 야유회를 가기위해 버스를 대절한다고 생각해봅시다. 야유회를 가기 위해 모인 인원은 125명이고 버스는 25인승입니다. 125명은 배정이 완료 되어서 버스를 5대를 대절했는데, 11명이 추가로 가고싶다고 합니다. 그래서 추가로 25인승짜리 버스 한 대를 대절했습니다. 여기서 총 승객 136명은 "길이"입니다. 그리고 버스가 한번에 태울 수 있는 승객은 "용량"입니다. 다시 Go언어로 돌아와서 make() 함수를 이용해 슬라이스를 선언한다고 생각해봅시다. 선언한 슬라이스의 용량이 25인데 101개의 값을 초기화하기 위해서는 125의 용량이 필요하게됩니다. 이러한 방식으로 메모리를 관리하는 것입니다. 용량은 "cap(컬렉션이름)"으로 용량을 알 수 있습니다.
그리고 주의해야할 점은 make() 함수를 이용해 슬라이스의 메모리를 할당하고 난 후에 []int{1,2,3,4}
와 같은 식으로 입력하여 값을 초기화하면 새로운 메모리를 할당하면서 그 전의 값은 없어집니다. 어느 부분에서든 동일하게 적용되는 당연한 것입니다. 기존의 메모리를 사용하고 값을 추가하기 위해서는 아래에서 배우는 append() 함수를 사용해야합니다.
아래 코드를 바로 실행해보세요.
슬라이스 추가, 병합, 복사
또한 append() 함수를 이용해서 슬라이스에 데이터를 추가할 수 있습니다. 위 예시를 보고 이미 눈치챘을 겁니다. 슬라이스 용량이 남아있는 경우에는 그 용량 내에서 슬라이스의 길이를 변경하여 데이터를 추가하고, 용량이 초과하는 경우에는 설정한 용량만큼 새로운 배열을 생성하고 기존 배열 값들을 모두 새 배열에 복제한 후 다시 슬라이스를 할당하는 방식입니다.
그리고 데이터를 추가할수 있을 뿐만이 아니라 슬라이스에 슬라이스를 추가해서 붙일 수 있습니다. 여기서 슬라이스에 슬라이스를 추가하기 위해 주의할 점은 추가하는 슬라이스 뒤에 "..."을 입력해야 한다는 것입니다. ...은 슬라이스의 모든 요소들의 집합을 표현하는 것으로 아래 예제의 "sliceB..."은 슬라이스의 요소 집합인 {4, 5, 6}으로 치환되는 것입니다. 따라서 사실상 슬라이스에 슬라이스를 추가하는 것이 아니라, sliceA에 {4, 5, 6}이라는 요소들이 추가되는 것입니다.
아래 코드를 바로 실행해보세요.
그리고 copy() 함수를 이용해 한 슬라이스를 다른 슬라이스로 복사할 수 있습니다. copy() 함수는 "copy(붙여넣을 슬라이스, 복사할 슬라이스)" 형식으로 사용합니다. 당연히 복사할 슬라이스와 붙여넣을 슬라이스 모두 선언이 선행돼야 합니다.
아래 슬라이스를 다른 슬라이스로 복사하는 예시 코드가 있습니다. 바로 실행해보세요.
슬라이스는 원래 '자르기'라는 뜻입니다. 그래서인지 슬라이스는 슬라이스의 부분만 잘라서 복사할 수도 있습니다. 이때 "붙여넣을 슬라이스 := 복사할 슬라이스[복사할 첫 인덱스:복사할 마지막 인덱스+1]"이라고 하면 잘라서 복사할 수 있습니다(':=' 용법을 이용해 바로 선언과 동시에 값을 저장함). 예를 들어 l := sliceA[2:5]
라고 한다면 슬라이스 l에 sliceA의 인덱스 2요소부터 4요소까지 잘라서 복사한다는 것입니다. 마지막 요소는 복사하지 않습니다. 그리고 처음과 마지막 인덱스를 생략하면 첫 요소와 맨 마지막 요소를 의미합니다. 예를 들어 l := sliceA[:5]
라면 sliceA의 처음부터 인덱스 4의 요소까지 복사한다는 것입니다. 반대의 경우도 마찬가지로 쓸 수 있습니다.
아래 코드를 바로 실행해보세요.
위 코드를 눈여겨 봤으면 특이한 점을 알았을 것입니다. 복사해온 슬라이스의 값을 바꿨는데 기존 복사한 슬라이스의 값도 바뀐 것을 확인할 수 있습니다. 왜냐하면 앞서 말했듯이, 슬라이스는 배열과 다르게 값을 복사해오는 것이 아니라 슬라이스 자체가 참조하고있는 주소값을 같이 참조하는 것을 의미하기 때문입니다. 하지만 같은 상황이라면 배열은 단순히 값을 복사해서 초기화합니다