Go - slice 알아보기

2023.01.04

슬라이스의 구조

슬라이스 자체는 실제 배열이 아닌 배열을 가리키고 있는 포인터이다.
실제로 슬라이스는 배열의 주소값을 가리키는 포인터배열의 길이(length int형) 그리고 실제 메모리에 할당된 배열의 최대 용량(capacity int형) 으로 이루어져있다.

slice struct


슬라이스 초기화

슬라이스는 s := []int{1,2,3} 와 같은 방법 또는 make() 메소드로 선언할수있다.
이때 주의해야할 점은 s := []int 와 같이 선언을하면 길이와 용량이 0인 아무값도 가지지 않은 nil slice 로 선언된다.
길이가 0 이고 참조할 배열도 가지고 있지 않기 때문에 s[0]=1 과 같은 요소 접근이 불가능하다.

s := []int
fmt.Println(s,len(s),cap(s))    //[],0,0  nil slice는 nil 과 같다
s[0]=1    //panic: runtime error: index out of range [0] with length 0

따라 슬라이스를 선언할때는 미리 초기화를 하거나 make() 메소드로 길이와 용량을 지정해준다.
make() 메소드는 첫번째 인자로 슬라이스의 타입, 두번째 인자로 슬라이스의 길이, 세번째로 슬라이스의 용량을 지정한다.
이때 용량을 생략하면 자동으로 길이와 같은 값으로 지정된다.

s := []int{1,2,3}
s2 := make([]int,3)    //용량이 자동으로 3이 됨 [0,0,0]

슬라이스를 s := make([]byte,5) 로 선언을 했다면 s는 다음과 같이 선언된다.

slice init

포인터는 실제 메모리에 할당된 배열의 주소를 가리키고 있으며 해당 배열의 길이와 용량을 가지고있다.


슬라이스의 길이(len)와 용량(cap)

슬라이스는 배열의 길이뿐만 아니라 용량이라는 값도 가지고 있는데 쉽게 말하자면 용량 은 실제 메모리에 할당된 배열의 크기이며 길이는 사용중인 요소의 개수이다. 용량이 10이고 길이가 3인 슬라이스가있다면 실제 메모리에는 길이가 10인 배열이 있고 그중 3개만 사용중이며 나머지 7개는 비어있는 상태이다.
각각 len()cap() 메소드로 슬라이스의 길이와 용량을 확인할수있다.


부분 슬라이스 자르기(sub slice)

sub := s[시작 인덱스:끝 인덱스+1] 로 슬라이스의 특정 부분 가져올수있다. 이렇게 잘라낸 슬라이스를 sub slice 라고하며 서브 슬라이스는 복사된 배열이 아닌 원본 배열의 부분을 참조하는 포인터가 된다.

s := []int{1,2,3,4,5}
s2 := s[1:3]    //s의 1번 인덱스(0부터 시작) 부터 3번 인덱스까지(1부터 시작) [2,3]이 된다

여기서 부분 슬라이스인 s2를 변경하면 원본 배열도 변경 된다. 반대로 원본배열이 수정되면 부분 슬라이스도 수정된다.

s2[0]=6
fmt.Println(s)    //[1,6,3,4,5]  원본 배열이 수정됨

시작 인덱스 또는 마지막 인덱스는 생략이 가능하다. 생략한다면 자동으로 맨 끝 인덱스가 들어간다.

s3 := s[:3]    //0부터 3번까지
s4 := s[1:]    //1번부터 끝까지
s5 := s[:]    //처음부터 끝까지

부분 슬라이스 동작 방식

slice init

이 이미지를 다시한번 보자. 길이와 용량이 5인 S 라는 슬라이스가 있다 여기서 s = s[2:4] 로 3번째부터 4번째 인덱스까지 부분 슬라이스로 가져왔다. 그러면 S 슬라이스는 아래와 같은 형태가 된다.

sub slice

기존 S 가 가리키던 배열은 그대로 있고 단지 포인터가 가리키는 부분이 바뀌었다. 실제 사용하는 길이는 2가 되었고 용량은 인덱스의 시작 부분부터 배열의 끝까지인 3이 되었다.


슬라이스 요소 추가(append)

append() 메소드로 슬라이스에 새로운 요소를 추가할수있다. 첫번째 파라미터로 슬라이스를, 두번째 파라미터에 추가할 요소를 넣는다.
두번째 파라미터이후로 계속해서 새로운 값을 추가할수도 있다.

s := []int{1,2,3,4,5}
s = append(s,6)    //[1,2,3,4,5,6]
s = append(s,7,8,9)    //[...6,7,8,9]

//슬라이스에 다른 슬라이스를 합치려면 (슬라이스명...) 으로 표현할수있다.
s2 := []int{10,20}
s = append(s,s2...)

append와 용량

앞서 슬라이스의 길이와 용량에 대해 설명을했다. 용량이란 일종의 여분과 같이 사용하지 않는 영역인데 만약 슬라이스에 append를 할경우 기존 용량이 남아 있다면 해당 영역에 새로운 요소를 채워넣고 용량이 부족하다면 현재 용량의 두배인 새로운 슬라이스를 생성하고 거기에 값을 복사한후 그 슬라이스를 사용하게 된다.
아래의 코드를 실행해보면 용량을 초과하는 append를 실행했을때 슬라이스의 포인터 주소와 용량이 바뀌는걸 확인할수있다.

s := make([]int,3,5)    //[0,0,0] 길이 3 용량은 5인 슬라이스
println(s)    //[3/5]0xc000100000

s = appned(s,1,2)    
println(s)    //[5/5]0xc000100000    <- 용량이 다 찼고 주소값은 바뀌지 않았다.


s = append(s,3,4,5)
println(s)    //[7/10]0xc000018050    <- 용량이 2배로 늘어나고 새로운 주소값으로 바뀌었다.

슬라이스 복사(copy)

copy() 메소드로 슬라이스의 내용을 다른 슬라이스로 복사할수있다.
정확히 말하자면 복사된 새로운 슬라이스를 생성하는것이 아닌 copy(a,b) 를 호출하면 b 슬라이스에 있는 내용들을 a 슬라이스에 복사하여 삽입한다. 이때 b의 길이가 a 보다 작다면 b의 길이 만큼 잘려서 복사된다.

a := []int{1,2,3}
b := make([]int,3,5)
c := make([]int,0,5)

copy(b,a)    //[1,2,3]
copy(c,a)    //[1]

슬라이스 요소 삭제

go에는 별도의 delete 기능이없다 그래서 직접 구현해야한다. 가장 기본적인 방법으로 delete 메소드를 정의한다면 전달받은 슬라이스를 삭제해야할 인덱스를 기준으로 2등분 으로 나누고 삭제할 인덱스를 건너뛴 다음 두개를 다시 합치는 식으로 구현할수 있다.

func delete(slice []int,index int) int[] {
    return append(slice[:index],slice[index+1:]...)
}

이 방법 외에도 여러가지 방법으로 요소 삭제를 구현할수있다. 필요에 따라 적합한걸 찾아서 써보자 더 많은 예제는 여기를 참조하자.


reference

Do you want something exciting?

© 2022. YSH All rights reserved.