본문 바로가기

갈아먹는 go [1] array와 slice

들어가며

개인적으로 go 프로그래밍 언어를 좋아합니다. 간결한 문법과 강력한 성능, goroutine과 channel을 통해서 동시성 프로그램을 손쉽게 구현할 수 있는 점이 매력적이었습니다. 하지만 go의 고급 패턴에만 관심이 있었지, 정작 기본기가 부족하여 인터뷰에서 간단한 질문도 제대로 답변하지 못했습니다. 부족한 기본기를 다시 채워넣기 위해서 꼼꼼하게 기본 개념들을 다지겠습니다.

 

가장 먼저 살펴볼 개념은 array와 slice입니다. 개념서부터 실제 인터뷰에서 나올 법한 질문들을 풀어보는 방식으로 진행하겠습니다. go 인터뷰를 준비하는 분들께 도움이 됐으면 좋겠습니다. 

 

모든 소스코드는 다음 레포에서 확인 가능합니다.

https://github.com/yeomko22/go_basics/tree/master/ch1_array_slice

 

yeomko22/go_basics

go basic sample codes for practice. Contribute to yeomko22/go_basics development by creating an account on GitHub.

github.com

go array

array 생성하기

func createArray() {
    // int 배열 선언 후 값 할당
	var a [5]int
	a[2] = 1
	fmt.Println("a", a, reflect.TypeOf(a))

	// int 배열 선언과 동시에 값 할당
	b := [5]int{1, 2, 3}
	fmt.Println("b: ", b, reflect.TypeOf(b))

	// ...을 사용하여 배열의 크기를 초기화 값의 개수에 맞춰서 동적으로 결정
	c := [...]int{1, 2, 3}
	fmt.Println("c: ", c, reflect.TypeOf(c))
}

// 출력 결과
a [0 0 1 0 0] [5]int
b:  [1 2 3 0 0] [5]int
c:  [1 2 3] [3]int

기억해야 할 것은 [] 뒤에 자료형을 붙여주며, 길이가 미리 정해진다는 것입니다. reflect를 이용하여 타입 검사를 해보면 길이가 n인 int 배열이라고 나옵니다. 

 

empty array 생성 및 타입 비교

func compareEmptyArrayType() {
	fmt.Println("크가가 다른 array간 타입 비")
	a := [1]int{}
	b := [3]int{}
	fmt.Println(a, reflect.TypeOf(a))
	fmt.Println(b, reflect.TypeOf(b))
	fmt.Println(reflect.TypeOf(a)==reflect.TypeOf(b))
}

// 출력 결과
[0] [1]int
[0 0 0] [3]int
false

배열 생성 시에 아무런 값도 전달하지 않으면 빈 배열이 생성됩니다. 이것의 타입을 검사하면 각자 길이가 n인  배열로 나오게 되며, 이 둘은 다른 타입입니다. 둘 다 동일한 int 배열이라 타입이 동일할 줄 알았는데 비교해보면 false가 반환됩니다.

 

array 스캔

func scanArray() {
	a := [5]int{1, 2, 3, 4, 5}
	for i, e := range a {
		fmt.Println(i, e)
	}
}

// 출력 결과
0 1
1 2
2 3
3 4
4 5

range를 사용하여 배열을 스캔할 수 있습니다. 이 때  index 값과  value 값이 리턴됩니다.

 

array 복사

func copyArray() {
	a := [5]int{1, 2, 3, 4, 5}
	b := a
	b[1] = 6
	fmt.Println(a)
	fmt.Println(b)
}

// 출력 결과
[1 2 3 4 5]
[1 6 3 4 5]

go 에서 배열 변수는 배열 전체를 뜻하며 첫번째 요소의 포인터가 아닙니다. 따라서 배열을 다른 변수에 대입할 경우 배열 전체를 복사합니다. 복사된 배열에서 특정 인덱스 값을 변경하더라도 원래 배열에 영향을 주지 않습니다.

go slice

slice란 배열과 같지만 길이가 고정되어 있지 않고 동적으로 크기가 늘어납니다. 또한 배열과 달리 레퍼런스 타입입니다. 이는 내부적으로는 배열을 통해서 값을 관리하며, slice 자체는 배열의 가장 첫번째 요소를 가리키는 포인터를 담게 됩니다.

 

slice 생성

func createSlice() {
	var a []int
	fmt.Println("a", a, reflect.TypeOf(a))

	b := make([]int, 5)
	fmt.Println("b", b, reflect.TypeOf(b))

	c := []int{1, 2, 3, 4, 5}
	fmt.Println("c", c, reflect.TypeOf(c))

	fmt.Println(reflect.TypeOf(a) == reflect.TypeOf(b))
	fmt.Println(reflect.TypeOf(b) == reflect.TypeOf(c))
	fmt.Println(reflect.TypeOf(a) == reflect.TypeOf(c))
}

// 출력 결과
a [] []int
b [0 0 0 0 0] []int
c [1 2 3 4 5] []int
true
true
true

slice는 array를 생성할 때와 유사한 방식으로 생성하지만, 배열의 길이를 미리 정하지 않습니다. 초기 값을 전달하여 생성할 수도 있습니다. 생성된 슬라이스의 자료형은 []int이며, 값이 들어있건, 길이가 작건, 크건 모두 동일합니다.

 

slice에 값 추가하기

func addValueToSlice() {
	a := []int{1, 2, 3}
	a = append(a, 4, 5, 6)
	fmt.Println(a)

	b := []int{7, 8, 9}
	a = append(a, b...)
	fmt.Println(a)
}

// 출력 결과
[1 2 3 4 5 6]
[1 2 3 4 5 6 7 8 9]

append 구분을 사용하여 slice에 새로운 값을 추가할 수 있습니다. 또한 다른 slice를 뒤에 이어붙일 수 있습니다. 

 

slice 복사하기

func copySlice() {
	a := []int{1, 2, 3}
	b := a
	b[0] = 5
	fmt.Println("a", a)
	fmt.Println("b", b)

	c := []int{1, 2, 3}
	d := make([]int, 3)
	copy(d, c)
	d[0] = 5
	fmt.Println("c", c)
	fmt.Println("d", d)
}

// 출력 결과
a [5 2 3]
b [5 2 3]
c [1 2 3]
d [5 2 3]

slice는 array와 달리 레퍼런스 타입입니다. 때문에 slice를 다른 변수에 그대로 대입하게 되면 값의 복사가 일어나지 않고 포인터만 복사되게 됩니다. 따라서 대입한 slice에서 특정 인덱스 값을 바꾸게 되면 원래 slice의 값도 변하게 됩니다 .(예제 코드에서의 a, b).

 

slice를 복사하고 싶다면 원본 slice와 크기가 동일한 빈 slice를 만든 다음,  copy 내장 함수를 이용하여 값을 복사합니다. 이 경우 복사한 slice에서 값을 변경하더라도 원본 slice의 값이 변하지 않습니다.

 

slice 용량

func sliceCapacity() {
	a := []int{1, 2, 3, 4, 5}
	fmt.Println(len(a), cap(a))
	a = append(a, 6, 7)
	fmt.Println(len(a), cap(a))
}

// 출력 결과
5 5
7 10

slice에는 용량이라는 개념이 있다. slice가 용량만큼 가득 차게 되면 미리 메모리 공간을 확보해 놓는 것이다. (c++의 vector 개념과 유사하다.) cap 명령어를 사용하여 현재 용량을 확인할 수 있다.

 

sub slice

func subSlice() {
	a := []int{1, 2, 3, 4, 5}
	b := a[1:3]
	fmt.Println(b)
	b[0] = 7
	fmt.Println(a)
	fmt.Println(b)
}

// 출력 결과
[2 3]
[1 7 3 4 5]
[7 3]

slice의 인덱스 값을 이용해서 일부분을 잘라서 사용할 수 있다. 이 때 주의할 점은 이 역시도 레퍼런스로 작동하기 때문에 sub slice의 값을 변경하면 원본 slice의 값도 함께 변경된다는 것이다.

마치며

지금까지 go 언어에서 배열을 의미하는  array와 slice에 대해서 알아보았습니다. 중요 내용만 다시 복습하면 생성 시점에서 길이를 정하면 array, 안정하면 slice, 모든 array는 각각의 길이에 따라서 자료형이 달라진다는 것, array는 배열 내 모든 값을 나타내고 slice는 레퍼런스를 나타낸다는 것 등이 있습니다.

 

사실 기초 개념이라는 것이 차분히 들여다보면 쉽게 이해가 가는 것인데 인터뷰에서 맞딱뜨리면 이것처럼 당황스러운 것이 없습니다. 기본을 탄탄히 다져서 go 인터뷰 질문들에 잘 준비해보록 합시다.