업무에 사용하는 Go 언어 6 – array slice map

Go 언어에서 array, slice, map과 같이 반복 가능한 데이터를 다루어보도록 합시다.
반복 가능한 데이터라 하면 메모리에 줄을 지어서 존재하는 데이터로 쉽게 이해할 수 있습니다.
이러한 데이터를 다루는 부분에서 Go 언어의 특성을 살펴볼 수 있습니다.

업무에 사용하는 Go 언어 썸네일

자료구조의 중요성

자료구조는 코드의 효율성에 가장 관련이 있다고 생각한다.
효율성에는 최적화, 가독성 등 많은 의미가 담겨있다.
필자가 생각하기에는 자료구조에 대한 정확한 지식이 없으면 메모리가 많이 고생할 것으로 본다.
이러한 부분에 대해서는 어떠한 에러도 발생하지 않기 때문에 오로지 엔지니어의 실력으로 판단해야 할 것이다.

프로젝트 진행

프로젝트에 array.go, slice.go, map.go 각 파일을 만들어서 진행합니다.

array

직역하면 배열이라고 하는 array는 정해진 타입의 데이터가 정해진 크기만큼 반복하는 구조입니다.
Go 언어에서 array는 고정된 크기를 가지기 때문에 선언과 동시에 메모리 할당이 진행되며 인덱스를 통해 접근이 가능합니다.

array의 기본 문법과 예시는 다음과 같습니다.

Go
// 기본 문법
var array_name [n]T

// 예시
var a [10]int

T는 타입, n은 길이를 의미합니다.
따라서, 위의 코드는 변수 a를 길이가 10인 int 배열로 선언한 것입니다.
여러가지 다른 타입을 사용해보기도 하고 코드를 짧게 선언해보기도 하면서 직접 사용해봅시다.

Go 언어 array 예시
Go 언어 array 예시

slice

array는 고정된 크기를 가지고 있는 반면에 slice는 array의 값들을 동적으로 다룰 수 있습니다.
“array의 값들을 다룬다.”라고 표현한 이유는 Go 언어 slice는 그 자체로 데이터를 저장할 수 없습니다.
array의 상위 개념으로, array를 참조하여 존재하기 때문입니다.

이러한 원리로 인하여 가변 길이를 가질 수 있고, append와 같이 내장 함수를 통해 새로운 값을 추가할 수도 있습니다.
만약 서로 다른 slice가 같은 array를 참조하고 있다면 slice 간에는 서로 영향을 주고 받기 때문에 정확한 개념 이해가 필요하겠습니다.

slice의 개념과 특징들이 어느 정도 파이썬과 비슷하고 편리하기 때문에 array보다는 흔하게 사용됩니다.

slice의 기본 문법과 예시는 다음과 같습니다.

Go
var slice_name [ ]T

가변 길이이기 때문에 기본 문법에는 array와 달리 n이 존재하지 않습니다.
만약, 참조하고자 하는 array의 일부만 참조하고 싶다면 다음과 같이 범위를 지정할 수 있습니다.

Go
a[low : high]
Go 언어 slice 예시
Go 언어 slice 예시

여러 개의 소수를 담은 primes array의 1번지부터 3번지까지 데이터만 slice에서 참조하여 출력하는 예시입니다.
이와 같이 slice는 어떠한 데이터도 저장하지 않으며 array의 한 영역을 나타낼 뿐이라는 개념이 핵심입니다.

Go언어 slice에서 가장 중요한 부분은 참조이기 때문에 범위에 대한 기본 값을 이해할 필요가 있습니다.
[low:high]와 같이 표현을 했을 때 low의 기본 값은 0, high의 기본 값은 전체 길이가 되겠습니다.
따라서 다음과 같이 a라는 array에 대해 아래 4개의 slice는 모두 동일한 범위를 의미하게 됩니다.

Go
var a [10]int

a[0:10]
a[:10]
a[0:]
a[:]

이러한 표현을 slice expression(슬라이스 표현)이라고 하며 이는 파이썬과 상당히 유사한 것을 알 수 있습니다.

slice literal

다른 방법으로 literal(리터럴)을 이용하여 array와 slice를 한 번에 생성할 수 있습니다.
앞의 예시를 다음과 같이 한 번에 처리할 수 있습니다.

Go 언어 slice 예시
Go 언어 slice 예시

짧은 변수 선언에 대해서는 이제 더 설명하지 않도록 하겠습니다.
[ ]int{2, 3, 5, 7, 11, 13} 에 해당하는 부분은 6개의 소수를 가진 array를 선언하여 slice로 참조하는 것을 의미합니다.
여기에 추가로 영역까지 지정해주었습니다.
다음과 같이 다양한 literal을 정하여 활용할 수 있으니 꼭 사용해보고 넘어가도록 하세요.

Go
q := []int{2, 3, 5, 7, 11, 13}

r := []bool{true, false, true, true, false, true}

// struct(구조체)는 추후에 다룰 예정입니다.
s := []struct {
	i int
	b bool
}{
	{2, true},
	{3, false},
	{5, true},
	{7, true},
	{11, false},
	{13, true},
}

slice length, capacity

Slice의 길이(length)와 용량(capacity)는 개념적으로 꼭 설명되고 있는 부분입니다.
길이는 slice가 포함하는 요소의 개수를 의미하여 이는 array와 동일합니다.
용량은 첫 번째 요소부터 계산하여 slice가 참조하고 있는 array의 끝 요소까지의 개수를 의미합니다.
쉽게 생각하면 slice가 참조하고 있는 array가 담은 수 있는 데이터 양이라고 할 수 있겠습니다.

앞서 append와 같이 요소를 추가할 수 있다고 언급했습니다.
만약 요소를 추가하다 보니 slice의 용량을 넘어서면 어떻게 되는지 확인해보겠습니다.
다음 코드를 직접 실행하여 결과를 확인해봅시다.

Go
package main

import "fmt"

func main() {
	slice := []int{1, 2, 3}
	fmt.Println(slice)
	fmt.Println(len(slice))
	fmt.Println(cap(slice))

	slice = append(slice, 4, 5)
	fmt.Println(slice)
	fmt.Println(len(slice))
	fmt.Println(cap(slice))
}

실행 결과를 확인해보시면 slice의 용량이 3에서 6으로 증가한 것을 알 수 있을 것입니다.
slice는 용량을 늘려야 할 경우 초기 용량의 두 배로 용량을 증가시킵니다.

조금 더 관심이 있는 사람을 위한 이야기

내부적으로 slice의 용량이 늘어나면서 slice가 참조하는 메모리 주소 값이 변경된다.
이러한 과정은 사실 상 새로운 슬라이스를 정의하는 과정이며 ‘Re-slicing’이라고 한다.

map

map은 key, value 쌍으로 이루어진 자료 구조로, 특정 키에 대하여 값을 조회하는 데에 사용합니다.
Go 언어에서 map에 대하여 다른 언어와 유사하게 동적 크기, 해시 테이블을 통한 빠른 조회를 지원합니다.

map의 기본 문법과 예시는 다음과 같습니다.

Go
// 기본 문법
var map_name map[key_type]value_type

// 리터럴을 사용한 예시
var m = map[string]string{
    "a": "Apple",
    "b": "Banana"
}

예시의 경우 리터럴을 사용하여 초기화된 map을 선언했습니다.
만약 초기화할 어떠한 값도 없는 map의 경우 어떻게 선언을 해야하는지 의문이 생길 수 있습니다.
이는 go 언어의 내장 함수 make를 사용하여 선언하는데 이는 다음에 알아보도록 하고 map을 사용하는 방법만 알아보도록 합시다.

궁금하신 분들을 위해…

map 또한 메모리에 있는 헤시테이블을 참조하는 원리이다.
slice와 map 모두 어떠한 값으로든 초기화가 되어야 사용이 가능하다.
만약, 초기화 없이 선언만 할 경우 기본 array, 기본 헤시테이블이 없기 때문에 slice와 map은 nil 값을 가지게 되므로 쓸모가 없어진다.
nil 값은 일단 쓰레기 값으로 이해하고 넘어가자.

make 함수는 주어진 타입(혹은 규칙)대로 초기화하여 사용할 수 있는 데이터를 반환한다.
slice, map, channel에 대해 사용된다.

Go 언어 map 예시

위의 예시를 참고하여 직접 map을 사용해봅시다.
Go언어에서도 다른 언어처럼 새로운 키에 대한 값을 정의하거나 기존 키에 대한 값을 제거할 수도 있습니다.

한 가지 주의할 점은 map에 없는 키에 대하여 접근할 경우 값에 해당하는 타입의 zero value를 반환합니다.
최악의 경우 존재하는 키에 대한 값과 존재하지 않는 키에 대한 값이 일치할 수도 있습니다.
이럴 때 다음과 같이 존재 여부와 함께 값을 조회할 수 있습니다.

Go
value, is_exist := m["c"] // c라는 키의 값과 존재여부를 함께 받아온다.

다음 글에 알아볼 내용

Go 언어 nil 상태