티스토리 뷰

메모

Go - slice 기본 내용 정리

4567은 소수 2022. 1. 26. 19:56

요즘 go 문법 공부 중인데 slice를 처음보고 파이썬 리스트랑 비슷하다 생각했지만 개념적으로 좀 다른 부분이 있어 기본적인 slice 개념을 정리를 하고자 한다. 

 

1. 배열과 슬라이스 차이

 

배열 : 정적 할당

슬라이스 : 동적 할당 

인 줄 알았는데 조금 다르다.

 

슬라이스의 구조는 다음과 같다. (reflect.SliceHeader)

type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

Data : 슬라이스가 가리키는 배열의 시작 포인터

Len : 슬라이스의 길이

Cap : 슬라이스의 capacity

 

이 구조로 인해 어떤 경우에는 슬라이스 간에 영향이 생기고 어떤 경우에는 영향이 안 생긴다. (밑에서 설명)

 

그리고 슬라이스는 해당 배열을 가리키는 포인터를 가지므로 서브함수에 파라미터로 넣어도 같은 배열을 가리키는 포인터, Len, Cap 이 복사된다. 그러므로 call by reference 처럼 활용될 수 있다.

package main

import (
	"fmt"
)

func ArrayTest(array [3]int) {
	array[0] = 0
}

func SliceTest(slice []int) {
	slice[0] = 0
}

func main() {
	array := [3]int{100, 200, 300}
	slice := []int{111, 222, 333}

	ArrayTest(array)
	SliceTest(slice)

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

array의 경우 서브함수의 파라미터로 해당 배열의 주소가 아닌 그냥 배열로 받으므로 서브함수를 실행할 때 기존 배열 값을 복사하여 진행하므로 ArrayTest 를 빠져나와도 값에 영향이 없다.

 

하지만 slice의 경우 slice가 가리키는 포인터마저 복사가 되므로 서브함수에서 slice[0]=0 으로 변경한 값이 그대로 적용됨을 알 수 있다.


2. 슬라이스 선언

 

슬라이스는 기존 배열과 비슷하게 선언이 가능하며, make를 이용해서 생성할 수도 있다.

package main

import (
	"fmt"
)

func main() {
	var array1 = [3]int{1, 2, 3}
	var slice1 = []int{1, 2, 3}
	var slice2 = make([]int, 3)
	var slice3 = make([]int, 3, 5)
	var slice4 = []int{0: 111, 1: 222, 3: 444}

	fmt.Println("array1, len, cap : ", array1, len(array1), cap(array1))
	fmt.Println("slice1, len, cap : ", slice1, len(slice1), cap(slice1))
	fmt.Println("slice2, len, cap : ", slice2, len(slice2), cap(slice2))
	fmt.Println("slice3, len, cap : ", slice3, len(slice3), cap(slice3))
	fmt.Println("slice4, len, cap : ", slice4, len(slice4), cap(slice4))
}

slice1 :  기존 배열에서 크기 할당 해주는 것 대신 빈 칸으로 생성 => Len, Cap 모두 초기화한 크기만큼

slice2 : make를 이용해 생성, 크기 생성한 만큼 Len, Cap 생성, 값은 0으로 초기화

slice3 : make를 이용하지만 make(Type, len, cap) 형태로 생성, Len은 3, Cap은 5인 슬라이스 생성, 값은 0으로 초기화

slice4 : 각 인덱스에 맞는 값으로 지정하여 생성, 지정 안 한 값은 0으로 초기화, Len, Cap은 마지막 인덱스까지 길이로 생성

 

slice는 그 자체로 리스트 형태를 갖는 것이 아닌 배열을 따로 생성하여 그 배열을 가리키는 포인터를 갖는 것이다. 그러므로 위에서 생성된 [1 2 3], [0 0 0] 등은 모두 그 만큼의 Len, Cap을 갖는 배열이 따로 생성된 것이다.


3. 슬라이스 순회

 

range를 이용해 슬라이스를 순회하는 것은 배열과 동일하다.

package main

import (
	"fmt"
)

func main() {
	var slice1 = []int{1, 2, 3}

	for i, v := range slice1 {
		fmt.Println("index, value : ", i, v)
	}
}


4. append

 

슬라이스에는 append를 이용할 수 있다. 하지만 주의할 점은 c++의 vector나 파이썬의 list에서 append를 사용하면 메서드로써 해당하는 리스트에 값을 붙히는 것이지만

go에서는 가리키는 배열에 capacity가 남아 있으면 그 배열에 추가를 하고 동일한 배열을 가리키는 슬라이스를 리턴하고,

그렇지 않으면 기존 capacity의 2배를 갖는 새로운 배열을 생성하여 그 배열을 가리키는 슬라이스를 새로 생성한다.

여기서 위에서 말한 영향을 받냐 안 받냐의 문제가 생긴다.

package main

import (
	"fmt"
)

func main() {
	var slice1 = []int{1, 2, 3}

	fmt.Println("slice1, len, cap : ", slice1, len(slice1), cap(slice1))

	slice1 = append(slice1, 4)

	fmt.Println("slice1, len, cap : ", slice1, len(slice1), cap(slice1))

	slice1 = append(slice1, 5, 6)

	fmt.Println("slice1, len, cap : ", slice1, len(slice1), cap(slice1))
}

기본적으로 append를 통해 값을 추가할 수 있다. 3번째 예시와 같이 여러 값을 추가할 수도 있다. 하지만 2번째 예시의 경우 처음 선언한 slice1의 capacity보다 초과하여 append하여야 하므로 capacity가 2배가 되고 추가함을 확인할 수 있다.

 

그리고 슬라이스는 그 자체로 리스트가 되는 것이 아닌, 배열을 가리키는 포인터를 갖기 때문에 append 과정에서 capacity가 모자른지 아닌지에 따라 다음과 같은 문제가 발생할 수 있다.

 

1) capacity 모자르지 않은 경우

package main

import (
	"fmt"
)

func main() {
	slice1 := make([]int, 3, 5)

	slice2 := append(slice1, 111, 222)

	fmt.Println("slice1, len, cap : ", slice1, len(slice1), cap(slice1))
	fmt.Println("slice2, len, cap : ", slice2, len(slice2), cap(slice2))

	slice1[0] = 1
	slice2[1] = 2

	fmt.Println("slice1, len, cap : ", slice1, len(slice1), cap(slice1))
	fmt.Println("slice2, len, cap : ", slice2, len(slice2), cap(slice2))
}

capacity가 모자르지 않은 경우, slice2가 slice1에 append 한 배열을 가리키는 포인터를 갖는다고 하여도 결국 같은 배열을 가리키므로 서로에 영향을 준다. 3번째 결과와 4번째 결과를 보면 slice1, slice2 각각에 따로 값을 변경하였지만, 결국 둘은 같은 배열을 가리키는 포인터를 가지므로 서로에게 영향을 받음을 알 수 있다.

 

2) capacity가 모자른 경우 

package main

import (
	"fmt"
)

func main() {
	slice1 := make([]int, 3)

	slice2 := append(slice1, 111, 222)

	fmt.Println("slice1, len, cap : ", slice1, len(slice1), cap(slice1))
	fmt.Println("slice2, len, cap : ", slice2, len(slice2), cap(slice2))

	slice1[0] = 1
	slice2[1] = 2

	fmt.Println("slice1, len, cap : ", slice1, len(slice1), cap(slice1))
	fmt.Println("slice2, len, cap : ", slice2, len(slice2), cap(slice2))
}

처음 선언한 slice1의 capacity가 3이고 length도 3이므로 더 이상 원소를 추가할 수 있는 가용 공간이 없다. 그러므로 slice2를 위와 같이 append로 선언하게 되면 slice1과 slice2는 서로 다른 배열을 가리키는 포인터를 갖는다.

(slice2는 slice1이 가리키는 배열의 cap이 2배, len과 원소는 복사된 새로운 배열을 가리키는 포인터를 갖고, 이후 append로 111, 222가 해당 배열에 추가됨)

 

그러므로 둘은 서로 다른 배열을 가리키므로 3, 4 번째 결과에서 서로 영향을 주지 않음을 알 수 있다.

 


5. 슬라이싱

 

슬라이스에서 배열 또는 슬라이스의 일부를 슬라이싱 할 수 있다. 

기본 형식은 array[start_index : end_index] , slice[start_index, end_index] 와 같은 형식이고, 이 때 len은 end_index - start_index 만큼, cap은 가리키는 start_index부터 배열의 끝까지 크기를 가진다.

 

그리고 0번째 index부터 슬라이싱하고 싶다면 array[ : end_index], 마지막 index까지 하고 싶다면 array[ start_inde : ] 으로도 할 수 있다. 전체를 슬라이싱하고 싶다면 array[ : ] 으로 할 수 있다.

 

그리고 array[start_index, end_index, max_index] 형식으로 capacity를 조절할 수도 있다.

이 때 capacity는 max_index - start_index 로, max_index부터는 새로운 값이 append 되면 새로운 배열을 가리키는 포인터를 갖게 된다. 

 

슬라이싱하는 경우도 capacity에 따라 복사한 값이 영향을 받을 수도, 안 받을 수도 있다.

package main

import (
	"fmt"
)

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

	fmt.Println("array, len, cap : ", array, len(array), cap(array))
	fmt.Println("slice1, len, cap : ", slice1, len(slice1), cap(slice1))
	fmt.Println("slice2, len, cap : ", slice2, len(slice2), cap(slice2))
}

slice1의 경우, len은 3, max_index를 따로 지정하지 않았으므로 capacity는 start_index부터 끝까지인 5이다.

그리고 slice2의 경우 max_index를 4로 지정하였으므로 capacity는 4-1=3 이다.

 

그러므로 현재까지는 아직 모두 같은 배열을 가리키고 있다. 그러므로 값을 변경하여도 아래와 같이 영향을 받는  결과가 된다.

package main

import (
	"fmt"
)

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

	array[0] = 100
	slice1[1] = 200
	slice2[1] = 300

	fmt.Println("array, len, cap : ", array, len(array), cap(array))
	fmt.Println("slice1, len, cap : ", slice1, len(slice1), cap(slice1))
	fmt.Println("slice2, len, cap : ", slice2, len(slice2), cap(slice2))
}

 

하지만 다음과 같이 capacity를 초과하게 되면 다른 배열을 생성하여 그 배열을 가리키는 포인터를 갖게 된다.

package main

import (
	"fmt"
)

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

	slice2 = append(slice2, 123, 456)

	slice1[0] = 100
	slice2[0] = 200

	fmt.Println("array, len, cap : ", array, len(array), cap(array))
	fmt.Println("slice1, len, cap : ", slice1, len(slice1), cap(slice1))
	fmt.Println("slice2, len, cap : ", slice2, len(slice2), cap(slice2))
}

slice2를 가용 가능했던 1개보다 더 많은 2개의 원소를 append 하였으므로 새로운 배열을 가리키는 포인터를 갖게 된다.

그러므로 array와 slice1은 같은 배열을 가리켜 영향을 받지만, slice2는 영향을 받지 않게 된다.

 


6. 복사

 

그렇다면 포인터로 계속 같은 배열을 가리키는 지 아닌지를 확인하기 번거롭거나 하여 그냥 메모리를 더 쓰고 값만 복사하고 싶다면 어떻게 해야할 것인가

 

1) for 문으로 돌면서 같은 인덱스에 같은 값을 넣는다.

2) append를 이용해 가리키는 배열을 새로 만들고 기존 배열의 값만 복사한다.

3) copy 함수를 쓴다.

package main

import (
	"fmt"
)

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

	slice2 := make([]int, len(slice1))

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

	fmt.Println("slice1, len, cap : ", slice1, len(slice1), cap(slice1))
	fmt.Println("slice2, len, cap : ", slice2, len(slice2), cap(slice2))

	slice3 := append([]int{}, slice1...) // 빈 슬라이스 하나 만들고 거기에 slice1... 으로 slice1 모든 값 저장

	slice4 := make([]int, 3, 5)
	slice5 := make([]int, 5)
	slice6 := make([]int, 3, 4)

	count4 := copy(slice4, slice1)
	count5 := copy(slice5, slice1)
	count6 := copy(slice6, slice1) // copy 리턴은 int, 복사된 원소 개수 리턴됨

	fmt.Println("slice3, len, cap : ", slice3, len(slice3), cap(slice3))
	fmt.Println("slice4, len, cap, count : ", slice4, len(slice4), cap(slice4), count4)
	fmt.Println("slice5, len, cap, count : ", slice5, len(slice5), cap(slice5), count5)
	fmt.Println("slice6, len, cap, count : ", slice6, len(slice6), cap(slice6), count6)

	slice1[0] = 100
	fmt.Println("slice1, len, cap : ", slice1, len(slice1), cap(slice1))
	fmt.Println("slice6, len, cap : ", slice6, len(slice6), cap(slice6))
}

slice2 ~ slice6에 모두 slice1을 복사하였다.

slice2는 간단히 for문으로 복사하였고, slice3는 append의 시작을 빈 슬라이스를 새로 만들어 slice1의 모든 값을 복사하는 방식으로 진행하였다.

slice4는 len 3, cap 5 이므로 slice1[0:3] 에 해당하는 값을 복사하였고, slice5는 len, cap 모두 5이므로 slice1의 모든 값을 복사하였다. slice6는 len 3, cap 4 이므로 slice[0:3] 에 해당하는 값을 복사하였다.

 

그리고 마지막 결과를 통해 slice1의 값이 바뀌어도 slice6에 영향을 받지 않음을 알 수 있고, 나머지 slice2 ~ slice5도 마찬가지로 모두 다른 배열을 가리키는 포인터를 갖는다.


6. 슬라이스 특정 위치 원소 제거 및 추가

 

특정 위치의 원소를 제거하거나 추가하는 메서드는 따로 없기 때문에 직접 구현해야 한다.

 

1) 특정 위치 원소 제거

 

1-1) 원소 앞 당기기 

 

그냥 단순히 idx 번째 원소를 삭제하고 싶을 때 idx + 1번째 원소부터 한 칸씩 앞으로 당기는 방법이다.

package main

import (
	"fmt"
)

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

	fmt.Println("slice, len, cap : ", slice, len(slice), cap(slice))

	for i := idx + 1; i < len(slice); i++ {
		slice[i-1] = slice[i]
	}

	slice = slice[0 : len(slice)-1]

	fmt.Println("slice, len, cap : ", slice, len(slice), cap(slice))
}

1-2) append를 이용해 특정 원소 제거

 

idx 번째 원소를 지운다고 가정했을 때

append의 시작 슬라이스를 slice[0:idx], 이어 붙힐 값을 slice[idx+1:]... 으로 잡으면 slice[0:idx] 에 해당하는 슬라이싱을 진행 후 idx + 1번째 원소부터 끝까지를 복사하는 의미이므로 idx번째 원소는 제거된다.

package main

import (
	"fmt"
)

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

	fmt.Println("slice, len, cap : ", slice, len(slice), cap(slice))

	slice = append(slice[:idx], slice[idx+1:]...)

	fmt.Println("slice, len, cap : ", slice, len(slice), cap(slice))
}

2) 특정 위치 원소 추가

 

2-1) 원소 뒤로 밀기

그냥 단순히 for문을 이용해 idx + 1번째 원소부터 뒤로 미는 것이다.

1개의 원소가 더 추가되므로 마지막에 0을 추가한 뒤 1칸 씩 뒤로 밀고, idx 위치의 값을 바꾸면 된다.

package main

import (
	"fmt"
)

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

	fmt.Println("slice, len, cap : ", slice, len(slice), cap(slice))

	slice = append(slice, 0)

	for i := len(slice) - 2; i >= idx; i-- {
		slice[i+1] = slice[i]
	}

	slice[idx] = 123

	fmt.Println("slice, len, cap : ", slice, len(slice), cap(slice))
}

 

2-2) append 이용해서 뒤로 밀기

 

특정 위치 원소 삭제와 마찬가지로 append를 이용해 뒤로 밀 수 있다.

append(slice[:idx], append(type{value}, slice[idx:]...)...) 형태로 사용하여 처음에 바꾸고자 하는 value를 가지는 len, cap 1 짜리 슬라이스를 만들고 거기에 idx번째부터 끝까지 값을 넣는다.

그리고 다시 slice[:idx] 에 위에서 만든 슬라이스의 모든 값을 붙혀넣으면 idx 번째 값은 value로 바뀌며, 한 칸 씩 밀린 것과 동일한 결과를 얻는다.

 

다만 여기서는 첫번째 append 과정에서 새로운 배열이 생성되므로 불필요한 메모리가 쓰이게 된다. (하지만 이 또한 가비지 컬렉터에 의해 사라진다.)

package main

import (
	"fmt"
)

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

	fmt.Println("slice, len, cap : ", slice, len(slice), cap(slice))

	slice = append(slice[:idx], append([]int{123}, slice[idx:]...)...)
	fmt.Println("slice, len, cap : ", slice, len(slice), cap(slice))
}

2-3) copy 이용하기

 

2-1의 for문을 이용하는 것과 비슷하게 copy를 이용할 수도 있다.

처음에 0을 추가해주고, copy(slice[idx+1:], slice[idx:]) 을 진행하게 되면 기존 idx 번째부터 끝까지가 복사되는데

처음 0을 추가하였어도 slice[idx+1:] 의 가용 공간이 모자르기 때문에 마지막 0은 복사되지 않는다. 

그리고 idx에 value를 수정하면 된다.

(이 경우 위와 다르게 불필요한 메모리가 사용되지 않는다.)

package main

import (
	"fmt"
)

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

	fmt.Println("slice, len, cap : ", slice, len(slice), cap(slice))

	slice = append(slice, 0)
	copy(slice[idx+1:], slice[idx:])
	slice[idx] = 123
	fmt.Println("slice, len, cap : ", slice, len(slice), cap(slice))
}


7. 슬라이스 정렬

 

Go 에서 기본적으로 제공하는 sort 패키지가 있다. 이를 이용해 슬라이스를 정렬할 수 있으며, 정렬 기준을 바꿀 수도 있다.

 

7-1) 단순 정렬

기본적으로 오름차순 정렬을 제공한다.

int 형은 Ints(x []int), float64는 Float64s(x []float64), string은 Strings(x []string) 등등 다양한 함수가 존재한다. 

package main

import (
	"fmt"
	"sort"
)

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

	sort.Ints(slice)

	fmt.Println("slice, len, cap : ", slice, len(slice), cap(slice))
}

package main

import (
	"fmt"
	"sort"
)

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

	sort.Sort(sort.IntSlice(slice))

	fmt.Println("slice, len, cap : ", slice, len(slice), cap(slice))
}

또는 이렇게도 가능하다.

 

 

7-2) 역순 정렬

내림차순 정렬은 위 정렬 기준에서 Reverse를 추가하거나, 비교에 필요한 Less, Len, Swap 함수를 직접 구현하여 역순 정렬이 가능하다.

package main

import (
	"fmt"
	"sort"
)

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

	sort.Sort(sort.Reverse(sort.IntSlice(slice)))

	fmt.Println("slice, len, cap : ", slice, len(slice), cap(slice))
}

package main

import (
	"fmt"
	"sort"
)

type sliceInt []int

func (slice sliceInt) Len() int           { return len(slice) }
func (slice sliceInt) Less(i, j int) bool { return slice[i] > slice[j] } // 내림차순, 오름차순은 부등호 반대
func (slice sliceInt) Swap(i, j int)      { slice[i], slice[j] = slice[j], slice[i] }

func main() {
	slice := sliceInt{3, 4, 1, 2, 5}

	sort.Sort(slice)

	fmt.Println("slice, len, cap : ", slice, len(slice), cap(slice))
}

직접 구현의 경우 []int 타입에는 기존에 Len, Less, Swap 메서드가 없으므로 sliceInt 라고 []int 와 동일한 타입을 정의한 뒤 메서드를 추가하여 구현할 수 있다. Sort 함수 이용 시 Len, Less, Swap 이 내장 함수로 정의되어 있고 기본으로 오름차순으로 정의가 되어있지만, 해당 타입에는 위 Len, Less, Swap 메서드를 이용해 정렬하겠다는 의미이다. 

 

이를 응용하여 구조체에도 특정 기준으로 정렬하는 것을 동일하게 적용할 수 있다.


 

참고 문헌 : Tucker의 Go 언어 프로그래밍

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
TAG
more
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함