프로그래밍 언어/Golang

Go: 배열 & 슬라이스 (Array, Slice)

배열

배열메모리 공간에서 연속된 위치에 있는 값 목록이다. 배열은 자료구조의 일종이며 Go 에서는 같은 타입에 대해 여러 개의 값을 담을 수 있다. 일반적인 형태의 변수를 사용한다면 변수를 여러 개 사용해야 할 것도 배열이 있으면 그 일을 보다 수월하게 처리할 수 있다.

 

아래의 배열은 5개의 숫자를 받을 수 있는데, Go 의 배열은 크기가 정해져있는 정적 배열이다. 크기가 변할 수 있는 동적은 것은 슬라이스라는 개념에서 나올 것이다.

package main

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

배열을 여러 줄로 사용하기 위해서는 맨 마지막 요소에 콤마를 반드시 찍어줄 필요가 있다.

package main

func main() {
	runes := [5]rune{
		'A',
		'B',
		'C',
		'D',
		'E',
	}
}

제로 값

배열만 선언 해놓고 값을 할당하지 않는 경우, 각 타입에 알맞는 제로 값이 설정된다. 아래의 경우 int 타입의 제로값은 0 이기 때문에 모든 원소가 0 이다.

package main

import "fmt"

func main() {
	var numbers [5]int

	// -> 0, 0, 0, 0, 0
	for _, num := range numbers {
		fmt.Println(num)
	}
}

인덱스 제로 베이스

더도말도 이야기 할 것도 없이 배열의 원소에 접근하기 위한 인덱스는 제로 베이스다. 0 부터 시작하며 length - 1 까지 해당한다. 따라서 길이가 5 인 배열의 최고 인덱스는 4 이며, 그 이상을 초과할 경우 에러가 발생한다.

package main

import "fmt"

func main() {
	var numbers [5]int
	numbers[0] = 10
	numbers[1] = 20

	// -> 10, 20, 0, 0, 0
	for _, num := range numbers {
		fmt.Println(num)
	}
}

슬라이스

슬라이스크기가 정해지지 않은 배열이며, 길이가 동적으로 변할 수 있다. Go 의 슬라이스가 가지는 가장 큰 특징은, 슬라이스는 그저 배열의 뷰(View) 일 뿐, 그 외에 다른 의미는 가지지 않는다는 점이다. 슬라이스는 기존의 배열을 기반으로 만들어질 수도 있고, 자체적으로 내부 배열을 가질 수도 있다. 꼭 기억해야 하는 사항은, 슬라이스는 내부적으로 배열을 기반으로 한다는 사실이다.

 

슬라이스는 배열과는 다르게 슬라이스를 생성하는 작업이 필요하다. make() 빌트인 함수를 사용하며, 그 이외에는 배열과 사용법이 같다. 아래의 슬라이스는 5개의 공간을 미리 확보해놓은 슬라이스를 생성한다.

package main

func main() {
	numbers := make([]int, 5)
}

하지만 리터럴의 형태로 사용하는 경우 make() 를 사용하여 슬라이스를 생성할 필요가 없다. 알아서 내부적으로 배열을 생성해주기 때문이다.

package main

import "fmt"

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

	// -> 0, 1, 2, 3, 4
	fmt.Println(numbers)
}

제로 값

make() 를 사용하지 않고 타입만 있는 형태의 변수로 선언한 슬라이스의 제로 값은 nil 이다.

package main

import "fmt"

func main() {
	var numbers []int

	// -> true
	fmt.Println(numbers == nil)
}

여기서 반드시 기억해야 하는 사항은, nil 슬라이스에 대해 길이를 계산하거나 값을 새로 추가해도 에러가 발생하지 않고 길이가 0 인 슬라이스처럼 취급한다는 점이다.

package main

import "fmt"

func main() {
	var numbers []int

	// -> 0
	fmt.Println(len(numbers))
}

기존의 배열에서부터

이미 만들어진 배열을 기반으로 슬라이스를 만들 수 있다. 문법을 자세히보면 어디선가 많이 본 것으로 느껴질지도 모른다. 이 문법은 파이썬에서 배열을 자를때 사용하는 문법이다. numbers[1:4] 의 의미는 인덱스 1 을 포함하고, 인덱스 4 미만까지 포함하라는 이야기다. 아래의 배열에서는 1, ,2, 3 의 값을 가진 인덱스가 그에 해당된다.

package main

import "fmt"

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

	// -> 1, 2, 3
	for _, num := range numbers[1:4] {
		fmt.Println(num)
	}
}

또한 기억해야 하는 사항은, 슬라이스를 변경하면 원본 배열도 바뀌고 원본 배열을 바꾸면 슬라이스도 바뀐다. 이는 그저 슬라이스가 배열의 일부분을 보여주는 것이기 때문이다.

package main

import "fmt"

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

	s[0] = 100
	s[1] = 200

	// -> 	 100, 200, 3
	// -> 0, 100, 200, 3, 4
	fmt.Println(s, numbers)
}

[1:4 라는 형태에서, [1:] 이 되면 인덱스 1에서 가장 마지막 인덱스까지를 의미하고, [:4] 가 되면 가장 첫 인덱스에서 3까지를, 마지막으로 [:] 가 되면 첫 인덱스에서 끝까지를 의미하는데 이는 배열에서 슬라이스로 변환할 때 사용하기도 한다.

내부 배열이 있는 슬라이스

슬라이스는 독립적으로 사용될 경우 내부 배열을 가지고 있다. 슬라이스에 값을 추가할 때는 append() 함수를 사용하는데, 이는 내부적으로 슬라이스의 내부 배열의 공간이 부족하면 새로운 배열을 만들고 복사하여 처리한다. 이러한 처리 때문에 일부 문제나 버그가 발생할 수 있으므로 슬라이스에 값을 추가할 떄는 똑같은 슬라이스에 재할당하는 것이 핵심이다.

package main

import "fmt"

func main() {
	numbers := make([]int, 0)
	for _, num := range [5]int{0, 1, 2, 3, 4} {
		numbers = append(numbers, num)
	}
	// -> 0, 1, 2, 3, 4
	fmt.Println(numbers)
}

추가적으로, 기존의 배열에서 가져온 슬라이스에 append() 를 통해 값을 추가하면 값이 추가된 인덱스의 값을 덮어씌워버리는 재미난 일이 발생한다. numbers[1:4] 의 슬라이스에 값을 추가하면 되면 4번 인덱스에 값이 들어가게 되어 기존의 값 4 는 날아가고 500으로 대체된다.

package main

import "fmt"

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

	// -> 1, 2, 3
	fmt.Println(s)
	s = append(s, 500)

	// -> 1, 2, 3, 500
	// -> 0, 1, 2, 3, 500
	fmt.Println(s, numbers)
}