슬라이스

Olivia·2024년 1월 19일
0

[Go]

목록 보기
3/4
post-thumbnail

Go에서 Slice는 배열을 가르키는 포인터 타입이다.
따라서 배열과 달리 배열의 길이를 확장하거나 축소하는 것이 가능하다.

  - 정적(Static): 컴파일 시점(Compile)에 결정
  - 동적(Dynamic): 실행 시점(Runtime)에 결정

Go에서 배열과 슬라이스는 다음과 같이 선언한다.

배열

var v[10]int

배열의 경우, 배열의 사이즈를 설정하고 선언한다.

import "fmt"

func main(){
	var a[3]string
    var b[]string
    
    fmt.Println(a) // [   ]
    fmt.Println(b) // []
}

Slice

슬라이스의 경우 배열을 가르키는 포인터 타입이다.

var v[]int

슬라이스의 경우 배열과 달리 선언할 때 사이즈를 설정하지 않고 선언한다.

배열 vs Slice

배열과 Slice의 차이를 알기 위해서는 다음 예시를 살펴보면 더 쉽게 받아들일 수 있을 것이다.

package main

import "fmt"

func changeArr(arr2 [4]int) {
	arr2[0] = 100
}

func changeSlice(slice2 []int){
	slice2[0] = 100
}

func main(){
	arr := [4]int{1, 2, 3, 4}
    slice := []int{1, 2, 3, 4}
    
    changeArr(arr)
    changeSlice(slice)
    
    fmt.Println(arr)
    fmt.Println(slice)
}

# result (go run main.go)
[1, 2, 3, 4] # arr
[100, 2, 3, 4] # slice

배열도 0번째 index값을 100으로 바꾸고, slice도 동일한데 왜 slice만 100으로 바뀐채 출력된 것일까?

그건 바로 포인터 개념 때문이다.

다음장에서 포인터 개념에 대해서 자세히 다루겠지만, 간단히 정리하자면, 포인터는 메모리 주소를 가리키는 것을 의미한다.

배열(Array)

배열의 경우 함수에 배열을 넘기면, 배열 전체가 복사된다.
그 이유는 배열은 같은 타입을 가지는 변수들의 묶음이기 때문이다.
그리고 이렇게 선언된 변수들은 각각 연속된 메모리공간에 할당 된다.
따라서, changeArr 함수에서 배열의 0번째 index를 100으로 변환한다고해도, 원본 배열에는 영향을 주지 않는 것이다.

만약, 아래와 같은 상황에서는 배열이 변경될 수 있다.

package main

import "fmt"

func changeArr(arr2 [5]int){
	arr2[0] = 100
    fmt.Println(arr2) // [100, 1, 2, 3, 4, 5]
}

func changeSlice(slice2 []int){
	slice2[0] = 100
}

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

	arr[0] = 200

	changeArr(arr)
	changeSlice(slice)
	
	fmt.Println(arr) 
	fmt.Println(slice)
}

# result (go run main.go)
[200, 2, 3, 4] # arr
[100, 2, 3, 4] # slice
  • main함수에서는 배열 arr과 슬라이스 slice를 만든다.
    여기서 arr는 [1, 2, 3, 4, 5]로, slice는 [1, 2, 3, 4, 5]로 초기화된다.

  • 이후 main함수에서 chageArr로 해당 함수를 호출하고 arr값을 전달한다.
    changeArr함수에서는 매개변수로 전달된 배열을 arr2값으로 복사되어 전달된다.
    따라서, changeArr함수 내부에서 arr2를 출력하면 변경된 값으로 [100, 2, 3, 4, 5]로 출력되지만,
    main함수에서는 changeArr함수에서 변경된 arr값이 원본 배열에 영향을 미치지않으므로 [1, 2, 3, 4, 5]가 출력되는 것이다.
    이는 값을 복사해서 사용하는 것이기 때문이 발생하는 현상이다.

  • 그러나 Slice는 다른 결과가 발생한다.
    changeSlice 함수는 slice의 값을 slice2로 참조를 받는다.
    이 말의 즉슨, slice2라는 새로운 인스턴스를 생성하는 것이 아니라 slice의 메모리 주소값을 복사하게 되는 것이다.
    따라서 값이 복사되지 않고 원본 값을 바라보고 있기 때문changeSlice 함수 내에서 slice2 값을 변경하면 slice값도 변경되어 [100, 2, 3, 4, 5]로 출력되는 것이다.


len과 Cap

Slice는 len뿐만 아니라 cap이라는 데이터를 가지고 있다.
- len : 현재 slice에서 사용중인 길이
- cap : 현재 slice의 총 길이(사용중인 길이 + 비어있는 길이)

make함수를 사용해서 slice를 생성해 caplen이 무엇인지 자세하게 살펴보자.

	package main
    
    import "fmt"
    
    func main(){
    	slice1 := []int{1, 2, 3, 4}
        slice2 := make([]int, 3, 10)
        slice3 := make([]int, 10)
        
        fmt.Println(slice1, len(slice1), cap(slice1), slice1)
        fmt.Println(slice2, len(slice2), cap(slice2), slice2)
        fmt.Println(slice3, len(slice3), cap(slice3), slice3)
        
    }
// go run main.go
[1 2 3 4 ] 4 4 [1 2 3 4]
[0 0 0] 3 10 [0 0 0]
[0 0 0 0 0 0 0 0 0 0] 10 10 [0 0 0 0 0 0 0 0 0 0]

💡 make

slice2 := make([]int, 3, 10)    

위의 코드는 make을 통해서 슬라이스를 생성하는 것이다.
10의 크기를 가지는 slice고, 그 중에서 3개의 요소만 사용할 수 있도록 하는 것이다.


Slicing

배열 일부를 잘라 Slice를 만드는 것이다.

package main

import "fmt"
    
    func main(){
    	array := [5]int{1, 2, 3, 4, 5}
		slice := array[1:3]

			fmt.Println(len(array), array)
			fmt.Println(len(slice), slice)
        
    }


// result $ go run main.go
5 [1 2 3 4 5]
2 [2 3]

여기서 array[1:3]의 의미를 살펴보자.
array[start:end]를 의미한다. 그러나 여기서 주의할 점은 end은 포함되지 않는 것이다.
위의 코드에서 [1:3]이라고 되어있는데 이는 index[0] = 2부터 index[3] =4 미만까지를 말하는 것이다.
따라서 slice는 [2 3]만 담기는 것.

만약, 처음부터 슬라이싱하고 싶다면,
array[0:3] 이나 0을 생략하고 array[:3]으로 표현할 수 있다.
마찬가지로 array[2:len(array)] 또는 array[2:]표현하면 마지막 index값까지 슬라이싱할 수 있다.

하지만, 여기서 slicing은 배열을 복사해서 새로운 변수로 만드는 것이 아니라, 기존 배열의 일부를 참조하기 때문에 메모리 공간을 할당하지 않는다.
따라서 슬라이스를 통해 배열의 일부를 수정하게 된다면, 원본 배열에도 영향을 미치게 되는 것이다.

package main

import "fmt"
    
    func main(){
    	array := [5]int{1, 2, 3, 4, 5}
			slice := array[1:3]

			fmt.Println(len(array), array)
			fmt.Println(len(slice), slice)
      
			slice[0] = 100
			slice[1] = 200

			fmt.Println(array)
    }
    
// result $go run main.go

5 [1 2 3 4 5]
2 [2 3]
[1 100 200 4 5]

slice를 통해서 [2 3]을 담은 후, slice의 값을 변경했더니 원본 array도 값이 변경되었음을 확인할 수 있다.


append

Slice의 끝에 요소를 추가한 뒤, 새로운 slice가 반환된다.

이때, 새로운 slice는 기존 slice와 동일한 메모리 주소를 사용하거나, 새로운 메모리 주소를 할당할 수 있다.

  • 동일한 메모리 주소 사용: Slice에 추가할 공간이 충분히 있을 경우
  • 새로운 메모리 주소 할당: Slice에 추가할 공간이 불충분할 경우
    -> 이 경우 새로운 메모리 주소에 기존 slice를 복사한 뒤 새로운 요소를 추가하게 된다.
package main

import "fmt"
    
    func main(){
    	array := [5]int{1, 2, 3, 4, 5}
			slice := array[1:3]

			fmt.Println(len(array), array)
			fmt.Println(len(slice), slice)

			slice2 := append(slice, 4, 5)

			fmt.Println(len(slice2), slice2)
      
    }

// result 
5 [1 2 3 4 5]
2 [2 3]
4 [2 3 4 5]

Slice 복사

Slice를 복사한다고해도 같은 메모리 주소를 공유하기 때문에 복사한 slice값이 변경된다면, 기존 slice의 값도 변경된다.
따라서 for문으로 값을 복사하거나, copy를 통해서 모든 값을 복사할 수 있다.

package main

import "fmt"
    
    func main(){
    	slice := []int{1, 2, 3}
			slice2 := make([]int, len(slice))

			for i, value:=range slice{
				slice2[i] = value
			}
			slice2[1] = 100
      
			fmt.Println(slice)
			fmt.Println(slice2)
			fmt.Println(slice)
    }
    
    // result 
    [1 2 3]
    [1 100 3]
    [1 2 3]
package main

import "fmt"
    
    func main(){
    	slice := []int{1, 2, 3}
			slice2 := make([]int, len(slice))

			copy(slice2, slice)

			slice2[1] = 100

			fmt.Println(slice)
			fmt.Println(slice2)
    }

// result
[1 2 3]
[1 100 3]

삭제

Slice에서 특정 요소를 삭제할 수 있다.

package main

import "fmt"
    
    func main(){
    	slice := []int{1, 2, 3, 4, 5, 6, 7}
			deleteIdx := 2

			fmt.Println(slice)
			for i:=deleteIdx + 1; i < len(slice); i++{
				slice[i-1] = slice[i]
				fmt.Printf("인덱스 %d 이후 이동: %v\n", i, slice)
			}
			slice = slice[:len(slice) - 1]
			fmt.Println(slice)
    }

// Result
[1 2 3 4 5 6 7]
인덱스 3 이후 이동: [1 2 4 4 5 6 7]
인덱스 4 이후 이동: [1 2 4 5 5 6 7]
인덱스 5 이후 이동: [1 2 4 5 6 6 7]
인덱스 6 이후 이동: [1 2 4 5 6 7 7]
[1 2 4 5 6 7]

추가

for문이나 append, copy를 통해서 새로운 요소를 추가할 수 있다.

append

package main

import "fmt"
    
    func main(){
    	slice := []int{1, 2, 3, 4, 5, 6, 7}
			insertIdx := 2

			fmt.Println(slice)
			slice = append(slice[:insertIdx], append([]int{100}, slice[insertIdx:]...)...)
			fmt.Println(slice)
    }

// result
[1 2 3 4 5 6 7]
[1 2 100 3 4 5 6 7]

copy

package main

import "fmt"
    
    func main(){
    	slice := []int{1, 2, 3, 4, 5, 6, 7}
			insertIdx := 2

			fmt.Println(slice)
			slice = append(slice, 0)
			copy(slice[insertIdx+1:], slice[insertIdx:]) 
			slice[insertIdx] = 100
			fmt.Println(slice)
    }
    
    // result
   	[1 2 3 4 5 6 7]
	[1 2 100 3 4 5 6 7]
 

이 부분 이해가 안가서 진짜 많이 헤맸다.
한 줄 한 줄 해석하자면,

  • slice := []int{1, 2, 3, 4, 5, 6, 7}
    slice를 정의한다. Slice는 1-7까지의 숫자를 포함하고 있다.
  • insertIdx := 2
    새로운 위치를 더할 위치를 insertIdx에다가 저장한다. 즉, slice[2] = 3을 말하고 있다.
  • slice = append(slice, 0)
    append 함수를 사용해서 slice 배열의 끝에 0을 추가한다. 이렇게 하면 배열의 크기가 하나 커지고, 새로운 값이 들어갈 공간이 생긴다.
  • copy(slice[insertIdx+1:], slice[insertIdx:]) <- 이 부분 너무 어려웠다.
    - slice[insertIdx+1:]은 slice[2+1]= slice[3:]이니까 slice의 3번째 인덱스값(4)에다가
    • slice[insertIdx:]은 slice[2:]로 index[2] 뒤의 모든 수 [3, 4, 5, 6, 7, 0]를 붙여라는 의미다.

      이렇게되면, [1, 2, 3, 3, 4, 5, 6, 7] 0은 한칸씩 뒤로 밀려나면서 복사되어 사라짐.

  • slice[insertIdx] = 100
    slice[2]번을 100으로 변경한다. 이렇게되면 [1, 2, 100, 3, 4, 5, 6, 7]이 된다.
  • fmt.Println(slice)
    slice값은 [1 2 100 3 4 5 6 7]이 된다.

정렬

go에서는 sort를 사용해서 정렬한다.

package main

import (
	"fmt"
	"sort"
)

func main(){
	slice:=[]int{2,4,1,5,9,8,3,6,7}

	fmt.Println(slice)
	sort.Ints(slice)
	fmt.Println(slice)
}

// result
[2 4 1 5 9 8 3 6 7]
[1 2 3 4 5 6 7 8 9]
package main

import (
	"fmt"
	"sort"
)

type Student struct {
	Name string
	Age  int
}

type Students []Student

func (s Students) Len() int           { return len(s) }
func (s Students) Less(i, j int) bool { return s[i].Name < s[j].Name }
func (s Students) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
func main() {
	students := []Student{{"c", 31}, {"a", 20}, {"d", 25}, {"b", 29}}

	fmt.Println(students)
	sort.Sort(Students(students))
	fmt.Println(students)
	
}

// result
[{c 31} {a 20} {d 25} {b 29}]
[{a 20} {b 29} {c 31} {d 25}]

sort.inteface를 구현하기 위해서는 Len, Less, Swap를 정의해야한다.

  1. Len: Slice 길이를 반환하는 메서드. 이를 통해서 정렬 범위를 지정한다.
  2. Less: 두 인덱스를 비교해서 정렬 순서를 결정한다.
    위의 예시에서는 i번째의 요소가 j번째보다 작은 여부를 반환한다.
  3. Swap : 두 인덱스(i, j)를 교환하는 메서드다. i번째 요소와 j 번째 요소를 교환해서 위치를 변경시킨다. 이 위치 변경을 통해서 조건에 맞도록 정렬할 수 있다.

reference
https://deku.posstree.com/ko/golang/slice/

profile
👩🏻‍💻

0개의 댓글