Slice

컴파일타임에 데이터 크기가 고정되어 런타임에 변경이 되지 않는 일반 배열과 달리 변경이 가능한 동적 배열 타입을 slice라고 한다. 정확하게 얘기하면 go에서 제공하는 배열을 가리키는 포인터 타입이다.


1. 선언 방법

var a []int         //길이가 0인 slice
fmt.Println(len(a))   //0

b := []int{1,2,3}   //길이가 3인 slice
fmt.Println(len(b))   //3

var c = []int{1, 5:2, 9:3}    //길이가 10인 slice
fmt.Println(c)    //[1 0 0 0 0 2 0 0 0 3]

배열선언방식과 비슷하지만 []안에 배열크기를 지정하지 않으면 slice로 선언이 된다.


◾ make()이용한 초기화

var a = make([]int,3)  //길이가 3인 slice
a[1] = 1
fmt.Println(a)    //[0 3 0]



2. 순회

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

for i :=0; i < len(slice); i++ {
  fmt.Print(slice[i]," ")   //1 2 3
}

fmt.Println()
for _,v := range slice{
  fmt.Print(v," ") //1 2 3
}



3. 슬라이스 요소 추가

배열과 달리 동적배열인 slice는 append()함수를 이용하여 배열에 값을 추가해줄 수 있다.

slice := int[]{1,2,3}
slice2 := append(slice,4)

fmt.Println(slice)    //[1 2 3]
fmt.Println(slice2)   //[1 2 3 4]

//기존의 변수에 값을 추가해주고 싶다면 아래와 같이 이용
slice = append(slice,4)
fmt.Println(slice)    //[1 2 3 4]

append()함수는 기존의 slice에 값을 추가해주는 것이 아니라 값을 추가한 slice를 반환해주는 함수라는 점을 명심해야한다.

append()는 빈공간이 충분하다면 빈공간에 요소가 추가되고 충분하지 않다면, 새로운 배열을 생성해서 반환하게 되고 이때 빈공간은 cap-len이다.



4. 동작원리

실제 값을 가르키는 pointer와 len으로 구성되어있다고 앞서 설명한 string처럼 slice도 실제 배열을 가르키는 포인터와 len으로 구성되어 있으며 cap이라고 capacity의 약자이며 최대 배열의 크기를 가르키는 변수가 한개가 더 존재하는 구조체로 구성되어 있다.

type SliceHeader struct{
    Data uintptr
    Len  int
    Cap  int
}

위 구조체가 slice의 구조로 실제 data는 배열포인터로 표현하는 것을 볼 수 있다. 그래서 *[10]int와는 엄연히 다른 데이터형태이다.


var slice = make([]int,3)   // len | cap이 3인 slice
var slice2 = make([]int,3,5)  //len이 3, cap이 5인 slice

make를 이용해 slice를 초기화하면 위와 같이 len과 cap을 별도로 정의해줄 수 있다.


func changeArray(array [5]int){
  array[2] = 200
}

func changeSlice(slice []int){
  slice[2] = 200
}

func main(){
  arr := [5]int{1,2,3,4,5}
  slice := []int{1,2,3,4,5}

  changeArray(arr)
  changeSlice(slice)

  fmt.Println(arr)      //[1 2 3 4 5]
  fmt.Println(slice)    //[1 2 200 4 5]
}

기본적으로 array타입을 함수의 매개변수로 주어지면 배열그대로를 복사해서 이용하는 call by value형태라서 값이 변하지 않지만, slice는 내부 구조를 이해하면 값이 변하는 이유를 이해할 수 있다.

slice는 내부에 배열을 직접 가지고 있는 것이 아니라 포인터형태로 값을 가르키고 있기 때문에 slice내부의 값이 변경되는 것이다. 따라서 실제 배열의 크기가 1024byte이더라고 slice변수의 데이터 크기는 24byte로 1024보다 현저히 적은 데이터 크기를 갖는다.



슬라이싱

슬라이싱은 배열의 일부를 가리키는 기능으로 슬라이싱의 결과는 슬라이스이다. [:]으로 표현할 수 있으며, :앞에는 시작 index가 오고 :이후에는 끝나는 index를 주는데 끝index는 포함하지 않는다.

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

fmt.Println(array)  //[1 2 3 4 5]
fmt.Println(slice, len(slice), cap(slice))  //[2] 1 4

slice[0] = 0
slice = array[1:2]
fmt.Println(array)  //[1 0 3 4 5]
fmt.Println(slice, len(slice), cap(slice))  //[0] 1 4

슬라이스는 특정 구간을 잘라낸 슬라이스를 반환하는 것이 아니기 때문에 slice의 값을 바꾸면 array의 값도 바뀌는 것을 볼 수 있다.

slice하게 되면 len은 요소 개수로 바뀌고 cap은 원본 배열의 len에서 시작 인덱스만큼 뺀 값으로 바뀌게 된다.


슬라이스도 배열과 마찬가지로 슬라이스를 수행할 수 있다.


특정위치부터 끝까지 슬라이스

array := [5]int{1,2,3,4,5}
slice1 := array[1:len(array)]
slice2 := array[1:]

끝까지 슬라이스 하고 싶을때는 배열길이를 주면 되지만 이는 위 예시처럼 생략이 가능하다.


전체 슬라이싱

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

배열을 슬라이스로 바꾸고 싶을때 사용할 수 있다.


캡사이즈 조절 슬라이싱

slice[시작인덱스:끝 인덱스: 최대 인덱스]

이처럼 세번째 인자로 최대 인덱스를 입력하면 cap사이즈가 최대인덱스-시작인덱스로 조절이 된다.



슬라이스 깊은 복사

1. for

slice1 := []int{1,2,3,4,5}
slice2 := make([]int{}, len(slice1))

for i,v := range slice1{
  slice2[i] = v
}


2. append()

slice1 := []int{1,2,3,4,5}
slice2 := append([]int{}, slice1...)

append가 슬라이스를 반환하는 점을 이용해서 빈 슬라이스에 기존의 slice를 append하면 깊은 복사를 수행할 수 있다.


3. copy()

slice1 := []int{1,2,3,4,5}
slice2 := make([]int, len(slice1))
copy(slice2,slice1)

go 내장 함수중에 copy()메서드를 이용하는 방법도 있다. 이때 copy() 리턴값은 복사한 개수이다.




Reference

『Tucker의 Go 언어 프로그래밍』 스터디 요약 노트