Go언어에서 배열은 고정된 크기 안에 동일한 데이터를 연속으로 저장해 배열의 크기를 필요에 따라 동적으로 증가시키거나 부분 배열을 발췌하는 등의 기능을 가지고있지 않다.
그런데 Slice
는 배열과 다르게 고정된 크기를 미리 지정하지 않고 이후에 필요에 따라 크기를 동적으로 변경할 수 있고, 부분 발췌가 가능
그리고 다차원 선언을 비롯한 배열의 모든 기능을 똑같이 구현할 수 있습니다.
따라서 슬라이스는 배열의 여러 제약점들을 넘어 여러 값을 다룰 때 개발자에게 주로 쓰입니다.
이러한 장점을 가진 (상대적으로)좋은 자료형인 슬라이스는 지금까지 배운 자료형과 내부적인 구조가 다르기때문에 선언 및 초기화를 할 때 주의!!
지금까지 배운 자료형의 선언 및 초기화방법과 비교해서 설명하겠습니다.
정수형(int, int32 ...등등)과 실수형(float32, float64...등등), 배열 등과 같은 자료형을 선언할때 "var 변수이름 자료형"
형식으로 선언
var num int
와 같은 형태로 입력"한개의 int형의 변수가 들어갈 메모리를 만들었다."
다른 자료형과 마찬가지로, 배열도 크기를 지정하고 선언하기 때문에 명시한 개수만큼의 메모리를 만든다.
var arr2 [3]int
라고 입력하면 3개의 int타입의 변수가 들어갈 메모리를 만들고, 초기화하지 않았기 때문에 자동으로 0이 할당됩니다. 따라서 선언만 했을 뿐인데 len()
함수를 이용해 배열의 크기가 3이라는 것을 확인할 수 있다.하지만 슬라이스를 위에서 설명한 것과 같은 방법으로 var a []int
와 같이 선언한다면 배열의 일부분을 가리키는 포인터를 만든다.
선언만 하고 초기화를 하지 않아서 슬라이스의 정보만 있는 배열만 생성되고, 실질적으로 어떠한 변수가 들어갈 공간(메모리)은 생성되지 않는다.
그렇다면 '다른 자료형은 메모리를 만들고 자동으로 0이나 ""을 할당하는데, 왜 슬라이스는 만들지 않을까?'
와 같은 궁금증이 생길 수 있습니다.
정말 간단합니다. why? 슬라이스는 크기를 미리 지정하지 않기 때문에 컴퓨터가 어디서부터 어디까지 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
를 반환!
코드
package main import "fmt" func main() { var a []int //슬라이스 변수 선언 아무것도 초기화 되지 않은 상태 a = []int{1, 2, 3} //슬라이스에 리터럴값 지정 a[1] = 10 //값이 할당되어 메모리가 생겼기 때문에 이렇게 접근 가능 fmt.Println(a) var b []int //nil slice 선언 if b == nil { fmt.Println("용량이", cap(b), "길이가", len(b), " Nil Slice입니다.") } }
출력
[1 10 3] 용량이 0 길이가 0 Nil Slice입니다.
슬라이스를 선언만 하면서 크기를 미리 지정할 수 있는 방법
즉, 값으 저장할 수 있는 메모리를 선언만 함으로써 생성
내장함수인 make()
함수를 이용하여 선언!
슬라이스를 생성함과 동시에 슬라이스의 길이(len), 슬라이스의 용량(cap)을 저장할 수 있다.
"make(슬라이스 타입, 슬라이스 길이, 슬라이스의 용량)"
슬라이스 용량(Capacity)는 생략해서 선언 가능
make()
함수를 이용해 선언한다면 비로소 모든 요소가 0인 슬라이스를 만들게 된다!
길이
- 초기화된 슬라이스의 요소 개수
- 즉, 슬라이스에 5개의 값이 초기화된다면 길이는 5!
- 그 후에 값을 추가하거나 삭제한다면 그만큼 길이가 바뀌게 된다.
len(컬렉션이름)
으로 길이를 알 수 있다.용량
- 슬라이스는 배열의 길이가 동적으로 늘어날 수 있다. 그렇기 떄문에 길이와 용량으로 구분한다.
- 예를 들어 동호회에서 야유회를 가기위해 버스를 태절한다고 생각해보자, 야유회를 가기 위해 모인 인원은 125명이고 버슨 25인승, 125명은 배정이 완료되어서 버스 5대를 대절했다. 그런데 11명이 추가로 가고 싶다한다. 그래서 추가로 25인승짜리 버스 한 대를 대절 -> 여기서 총 승객 136명은
길이
그리고 버스가 한번에 태울 수 있는 승객은용량
make()
함수를 이용해 슬라이스를 선언한다면, 선언한 슬라이스의 용량이 25인데 101개의 값을 초기화하기 위해서는 125용량이 필요!!- 이러한 방식으로 메모리를 괸리하는 것!, 용량은
cpa(컬렉션이름)
으로 용량을 알 수 있다.
주의!! make()
함수를 이용해 슬라이스의 메모리를 할당하고 난 후에 []int{1,2,3,4}
와 같은 식으로 입력하여 값을 초기화하면 새로운 메모리를 할당하면서 그 전의 값은 없어집니다.
이건 어느 부분에서든 동일하게 적용되는 당연한 것!!
기존의 메모리를 사용하고 값을 추가하기 위해서는 아래에서 배우는 append()
함수를 사용!!
코드
package main import "fmt" func main() { s := make([]int, 0, 3) // len=0, cap=3 인 슬라이스 선언 for i := 1; i <= 10; i++ { // 1부터 차례대로 한 요소씩 추가 s = append(s, i) fmt.Println(len(s), cap(s)) // 슬라이스 길이와 용량 확인 } fmt.Println(s) // 최종 슬라이스 출력 }
출력
1 3 2 3 3 3 4 6 5 6 6 6 7 12 8 12 9 12 10 12 [1 2 3 4 5 6 7 8 9 10]
append()
함수를 이용해서 슬라이스에 데이터를 추가할 수 있다.
슬라이스 용량이 남아있는 경우에는 그 용량 내에서 슬라이스의 길이를 변경하여 데이터를 추가하고, 용량이 초과하는 경우에는 설정한 욜얄ㅇ만큼 새로운 배열을 생성하고 기존 배열 값들을 모두 새 배열에 복제한 후 다시 슬라이스를 할당하는 방식!
그리고 데이터를 추가할 수 있을 뿐 아니라, 슬라이스에 슬라이스를 추가해서 붙일 수 있다!
주의점! 추가하는 슬라이스 뒤에 "..."을 입력! ...은 슬라이스의 모든 요소들의 집합을 표현하는 것으로, 사실상 슬라이스에 슬라이스를 추가하는 것이 아니라, silceA에 {요소값}들이 차가되는 것!
코드
package main import "fmt" func main() { sliceA := []int{1, 2, 3} sliceB := []int{4, 5, 6} sliceA = append(sliceA, sliceB...) //sliceA = append(sliceA, 4, 5, 6) fmt.Println(sliceA) // [1 2 3 4 5 6] 출력 }
출력
[1 2 3 4 5 6]
copy()
함술흘 이용해 한 슬라이스를 다른 슬라이스로 복사할 수 있다.
copy(붙여넣을 슬라이스, 복사할 슬라이스)
형식으로 사용!
복사할 슬라이스와 붙여넣을 슬라이스 모두 선언이 선행되어 있어야 한다!
코드
package main import "fmt" func main() { sliceA := []int{0, 1, 2} sliceB := make([]int, len(sliceA), cap(sliceA)*2) //sliceA에 2배 용량인 슬라이스 선언 copy(sliceB, sliceA) //A를 B에 붙여넣는다 fmt.Println(sliceB) // [0 1 2 ] 출력 println(len(sliceB), cap(sliceB)) // 3, 6 출력 }
출력
[0 1 2] 3 6
슬라이스는 원래 자르기
라는 뜻!
슬라이스는 슬라이스의 부분만 잘라서 복사할 수 있다.
이때 붙여넣을 슬라이스 := 복사할 슬라이스[복사할 첫 인덱스:복사할 마지막 인덱스+1]
l := sliceA([2:5]
라고 한다면 슬라이스 l에 sliceA의 인덱스 2요소부터 4요소까지 잘라서 복사! 마지막 요소는 복사하지 않는다!!l := sliceA[:5]
라면 sliceA
의 처음부터 인데스 4의 요소까지 복사한다는 것!코드
package main import "fmt" func main() { c := make([]int, 0, 3) //용량이 3이고 길이가0인 정수형 슬라이스 선언 c = append(c, 1, 2, 3, 4, 5, 6, 7) fmt.Println(len(c), cap(c)) l := c[1:3] //인덱스 1요소부터 2요소까지 복사 fmt.Println(l) l = c[2:] //인덱스 2요소부터 끝까지 복사 fmt.Println(l) l[0] = 6 fmt.Println(c) //슬라이스 l의 값을 바꿨는데 c의 값도 바뀜 //값을 복사해온 것이 아니라 기존 슬라이스 주솟값을 참조 }
출력
7 8 [2 3] [3 4 5 6 7] // 여기서 첫번째 인덱스[0]을 6으로! [1 2 6 4 5 6 7]
특이한 점이 보인다.
복사해온 슬라이스의 값을 바꿨는데 기존 복사한 슬라이스의 값도 바뀐 것을 확인할 수 있습니다.
why? 슬라이스는 배열과 다르게 값을 복사해오는 것이 아니라 슬라이스 자체가 참조하고있는 주소값을 같이 참조하는 것을 의미
하지만 같은 상황이라면 배열은 단순히 값을 복사해서 초기화합니다
어렵다!