Go를 배워보자 7일차 - 슬라이스(slice)

0

Go

목록 보기
7/12

슬라이스 ( slice )

1. 슬라이스란?

go에서는 값을 추가하여 확장할 수 있는 데이터 구조가 있는데, 이를 슬라이스라고 한다. 배열과 마찬가지로 슬라이스도 복수 개의 원소로 이루어져 모든 원소는 동일한 타입을 갖는다.

그리고 핵심적인 것은 슬라이스는 배열과 달리 슬라이스 끝에 원소를 추가할 수 있다는 것이다.

2. 슬라이스의 선언과 초기화, 할당

슬라이스의 선언은 배열과 다를 바 없다. 단지 배열의 선언에서 사이즈를 안써주면 슬라이스가 되는 것이다.

var 슬라이스이름 []타입

타입 앞에 있는 [] 에 사이즈를 꼭 비워주면 슬라이스가 된다.
그러나, 슬라이스는 배열과 같이 []타입{값,값,...}으로 초기화할 수 없고 make(타입, 사이즈) 를 이용해야 한다.

make(타입, 사이즈) 에서 사이즈는 기본 슬라이스의 사이즈라고 생각하면 된다. 마치 c++의 벡터에서 기본 사이즈를 설정해줄 수 있는 것과 똑같다고 보면 된다.

왜냐하면 슬라이스는 내부적으로 배열을 갖고 있는 동적 배열이기 때문이다. 즉, 리스트가 아니다.

그럼 int형 슬라이스를 만들어보자

package main

import "fmt"

func main() {
	var notes []int = make([]int, 5)
	notes[0] = 1
	notes[1] = 2
	fmt.Println(notes) // [1 2 0 0 0]
}

make(타입, 사이즈)를 통해서 슬라이스를 할당해줄 수 있다는 것을 알도록하자
즉, 배열처럼 make를 사용하지 않고 바로 값을 넣을 수 있는 것은 아니다.

또한, 슬라이스는 단축 변수 선언도 가능하다.

package main

import "fmt"

func main() {
	notes := make([]int, 5)
	notes[0] = 1
	notes[1] = 2
	for index, value := range notes {
		fmt.Println(index, value)
	}
}

결과

5
0 1
1 2
2 0
3 0
4 0

다음과 같이 make를 통해 단축 선언도 가능하고, 슬라이스에 range, len도 적용가능하다는 것을 확인할 수 있다.

3. 슬라이스 리터럴

배열과 마찬가지로 슬라이스 역시도 값을 미리 알고 있는 겨웅에 슬라이스 리터럴을 사용하면 슬라이스를 선언과 동시에 초기화를 할 수 있다.

슬라이스 리터럴은 배열 리터럴과 매우 유사하지만 사이즈를 적어주지 않는다는 차이점이 있다.

슬라이스 리터럴을 사용하면 자동으로 슬라이스를 만들어서 반환해주기 때문에 make를 사용하지 않아도 된다.

package main

import "fmt"

func main() {
	var notes []int = []int{1, 2, 3}
	names := []string{"hello", "high"}
	fmt.Println(notes) // 3 [1 2 3]
	fmt.Println(names) // 2 [hello high]
}

배열의 리터럴에서 사이즈만 넣어주지 않으면 된다.

4. 슬라이스 연산자 ( 슬라이싱 )

왜 우린 슬라이스를 처음부터 배우지 않았고 배열부터 배웠을까??
사실상 배열이 지원하는 모든 기능을 슬라이스에서 해줄 수 있는데 말이다.

c++에서는 stl 중에 vector라는 것이 있다. vector는 배열의 모든 기능을 지원하면서 동적 사이즈 배열을 지원하여 배열의 끝에 계속해서 원소를 넣을 수 있다.

그러나 vector는 배열보다 속도가 느리다. 임베디드 시스템에서 vector를 아무 생각없이 썼다가는 좋은 성능을 기대하지 못할 때가 있다. 왜 그럴까
??

vector는 내부의 자료구조로 배열을 사용한다. 이 배열의 사이즈를 늘리고 줄이는 기능은 결국 새로운 배열을 할당한 다음 값을 copy하는 과정을 반복하는 것이다.

슬라이스 역시도 마찬가지이다. 결국 내부 배열을 기반으로 구현이 되어있기 때문에 배열의 특성을 알아야하는 것이었다.

재밌는 것은 make 함수와 슬라이스 리터럴로만 슬라이스를 만들 수 있는 것처럼 보이지만, 배열을 통해서도 만들 수 있다.

슬라이스라는 이름과 같이 배열을 슬라이싱하면 슬라이스가 만들어진다.

슬라이스는 python에서의 슬라이스와 동일한 방식을 취한다.

sliced := array[i:j]

i번째 인덱스부터 j-1번째 인덱스까지의 값을 포함한 슬라이스를 만들라는 것이다. 즉, sliced는 슬라이스이다. 헷갈리지 말아야 할 것은 j는 포함하지 않는다.

package main

import "fmt"

func main() {
	var notes [10]int = [10]int{1, 2, 3, 4}
	slice1to3 := notes[1:3]
	slice1toEnd := notes[1:]
	slicefirstTo4 := notes[:4]
	fmt.Println(slice1to3) // [2 3]
	fmt.Println(slice1toEnd) // [2 3 4 0 0 0 0 0 0]
	fmt.Println(slicefirstTo4) // [1 2 3 4]
}

slice1to3는 notes배열의 1번째 2번째 인덱스 값을 가지고 오는 것이므로 2 3이 나온다.
slice1toEnd는 1~끝까지 값을 가져오는 것인데 이와 같은 경우는 마지막 [i:j]에서 j를 안써주면 된다.
slicefirstTo4에서는 맨앞~3번째 인덱스의 값까지 가져오는데, 맨 앞부터 가져오기 위해서는 [i:j]에서 i를 안써주면 된다.

중요한 것은 슬라이싱을 해서 가져온 것들 모두 슬라이스라는 것이다.

이것이 뭐가 그렇게 중요한가 싶을지도 모르겠지만, 정말 중요하다. 우린 배열을 슬라이싱해서 슬라이스를 만드는 것이다

배열 -----슬라이싱[i:j]----> 슬라이스

이전에 슬라이스는 내부 배열을 갖는다고 했다. 그 내부 배열이 바로 슬라이싱할 때의 배열이고 범위는 i~j-1값이다.

즉, 슬라이싱은 배열에 대한 추상 자료 구조에 불과하다. 더 간단히 말하자면 배열을 wrapping하는 자료구조로 배열을 동적 배열로 만들기 위한 연산들을 가지고 있어 추상화시킨 것일 뿐이다.

그렇다면 슬라이스의 값이 바뀌면 내부 배열에 영향을 준다는 것이고, 이는 원본 배열에 영향을 준다는 것인가??

반대로 원본 배열의 값을 바꾸면 슬라이싱 내부 배열 값을 바꾼다는 말이므로, 슬라이싱의 내부 배열에 영향을 미치는 것인가??

한 번 실험을 해보자

package main

import "fmt"

func main() {
	var notes [10]int = [10]int{1, 2, 3, 4}
	slice1to3 := notes[1:3] // 2 3
	slice1to3[1] = 999      // 2 3 에서 3의 값을 999로 바꿈
	fmt.Println(notes)      // [1 2 999 4 0 0 0 0 0 0]

	notes[1] = 1000 // 1번째 인덱스값 변경
	fmt.Println(slice1to3) // [1000 999]
}

기가막히고 코가 막힌다. 다른 언어는 해당 값을 가진 또 다른 배열 또는 리스트를 반환해주는데, 여기는 슬라이싱 시에 슬라이스로 만들어버리면서 내부 배열를 원본 배열의 일부를 가져오게 된다.

즉, 돋보기로 쏙 보는 것과 같다.

참고로 슬라이싱은 슬라이스에서도 사용할 수 있다.

5. append 함수

슬라이스는 원소를 추가할 수 있는 자료구조이다. append() 함수는 슬라이스의 맨 뒤에 원소를 추가할 수 있는 기능을 한다.

slice = append(slice,,,, ...)

다음과 같은 방식으로 값을 추가할 수 있다.

package main

import "fmt"

func main() {
	slice := make([]string, 0)
	fmt.Println(len(slice)) // 0
	slice = append(slice, "hello", "world", "my", "name")
	fmt.Println(len(slice)) // 4
	fmt.Println(slice) // [hello world my name]
}

js나 python은 메서드로 append를 불러서 값을 추가했지만, 여기서는 내장함수 append를 받아서 값을 추가한 결과를 돌려받아야 한다.

슬라이스의 내부 배열은 사이즈를 변경할 수 없기 때문에 사이즈의 한계를 느끼면, append함수 내부에서 새로운 배열에 더 큰 사이즈를 할당한 다음, 이 배열에 기존의 값들을 복사해넣어 슬라이스의 내부 배열로 교체해준다.

재밌는 것은 위의 resize 연산이 발생하지 않는다면 기존의 내부 배열의 값에 영향을 준다는 것이다.

package main

import "fmt"

func main() {
	var notes [10]int = [10]int{1, 2, 3, 4}
	slice1to3 := notes[1:3] // 2 3
	slice1to3 = append(slice1to3, 10, 20)
	fmt.Println(notes) // [1 2 3 10 20 0 0 0 0 0]
}

slice1to3에 append를 한 것이지만, 내부 배열이 notes이기 때문에 notes의 값도 바뀌는 것이다. 4와 0의 값에 덮어씌게 되는 것이다.

그러나 resize가 발생하면 새로운 배열을 만들어서 값을 복사해 내부 배열을 교체하는 것이기 때문에 원본 배열에는 영향을 주지 않게 된다.

package main

import "fmt"

func main() {
	var notes [10]int = [10]int{1, 2, 3, 4}
	slice1to3 := notes[1:3] // 2 3
	slice1to3 = append(slice1to3, 10, 20)
	fmt.Println(notes) // [1 2 3 10 20 0 0 0 0 0]
	slice1to3 = append(slice1to3, 30, 100, 200, 300, 400, 500)
	fmt.Println(notes)     // [1 2 3 10 20 0 0 0 0 0]
	fmt.Println(slice1to3) // [2 3 10 20 30 100 200 300 400 500]
}

slice1to3 = append(slice1to3, 30, 100, 200, 300, 400, 500) 연산에서 resize가 발생하여 내부 배열을 교체하게 된다. 따라서 notes에 값이 반영되지 않은 것이다.

6. 슬라이스와 제로 값

만약 슬라이스를 선언만 해놓고, 슬라이스 리터럴이나 make함수로 슬라이스를 넣어주지 않으면 어떻게될까??

배열의 경우에는 사이즈를 정해주기 때문에 해당 사이즈에 모두 초기값이 들어간다. 이를 제로 값이라고 한다. int는 0이고 string은 빈 문자열 "" 이 들어간다.

슬라이스의 경우에는 사이즈가 없기 때문에 애매해진다. 따라서 [] 이라고 표시하고 이는 nil 값을 갖고있다고 볼 수 있다.

package main

import "fmt"

func main() {
	var array [10]int
	fmt.Println(array) // [0 0 0 0 0 0 0 0 0 0]
	var slice []int
	fmt.Println(slice)        // []
	fmt.Println(slice == nil) // true
}

다음의 예제에서 slcie가 아무 값도 없어서 []이 나왔고 nil과 일치함을 알 수 있다.

7. 가변 인자

가변 인자란 함수의 parameter의 수를 마음껏 받는 것을 말한다. 0~허용되는 만큼 받을 수 있다. 대부분의 프로그램 언어에서는 ...을 사용하고 값을 받을 때는, c에서는 va_list로 사용하여 받는다. 또는 배열이나 리스트로 받는다.

즉, append() 함수에서 두 번째 인자로 몇 개의 값이 들어가도 되었던 큰 이유는 두 번째 인자가 가변 인자였기 때문이다.

실제로 선언을 보면 다음과 같다.

append(slice []Type, elems ...Type) []Type

두번째 인자인 elems가 ...Type이다. ...는 가변 인자를 표현한다. 그렇다면 golang에서의 가변 인자는 무엇으로 받는가?? 한다면 바로 슬라이스이다.

즉, elem은 Type이라는 타입을 가진 슬라이스인 것이다.

package main

import "fmt"

func main() {
	total := 0
	total = addValue(total, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
	fmt.Println(total) // 55
}

func addValue(sum int, values ...int) int {
	for _, v := range values {
		sum += v
	}
	return sum
}

addValue에서 values가 바로 가변 인자를 받는 슬라이스인 것이다. 하나씩 값을 빼내어 sum에 더해주는 연산을 해준다.

그렇다면 가변 인자는 슬라이스도 받을 수 있을까?? 그건 안된다.

package main

import "fmt"

func main() {
	slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	total := 0
	total = addValue(total, slice) // error
	fmt.Println(total) // 55
}

func addValue(sum int, values ...int) int {
	for _, v := range values {
		sum += v
	}
	return sum
}

왜냐하면 가변 인자는 인자를 값으로 받고, 모아놓아서 슬라이스로 만들어주는 것이지, 슬라이스 대 슬라이스로 가변 인자에 전달할 수는 없다.

따라서, 다른 문법이 필요한데, "이것이 가변 인자로 들어갈 슬라이스 입니다" 라는 것을 알려줄 문법이 필요하다.

가변 인자는 ... 을 사용한다. 따라서 가변 인자로 들어가는 슬라이스 역시도 ...을 써주는 것이다.

slice...

이렇게 말이다. 이에 따라 위의 에러가 발생하는 예제를 고쳐보자

package main

import "fmt"

func main() {
	slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	total := 0
	total = addValue(total, slice...)
	fmt.Println(total) // 55
}

func addValue(sum int, values ...int) int {
	for _, v := range values {
		sum += v
	}
	return sum
}

total = addValue(total, slice...)부분이 핵심이다. 슬라이스로 들어갈 것이므로, 가변 인자들은 여러 값들을 모아 슬라이스를 만들지 말고 이 슬라이스를 그대로 받으라는 것이다.

이거저거 챙길 문법이 많아 복잡하긴 하지만, 상당히 재밌는 언어이다.

0개의 댓글