컬렉션

컬렉션

컬렉션인 Array, Slice, Map에 대해 알아보자.


컬렉션


Go언어에서는 두 개 이상의 변수를 모아 놓은 것'컬렉션'이라고 합니다. 배열을 포함한 컬렉션은 다수의 데이터를 저장하고 처리하는 경우에 유용하고 굉장히 많이 쓰입니다. 컬렉션은 그 기능에 따라 이름이 다른 세 가지 용법이 있습니다.

1. 배열(Array)
2. 슬라이스(Slice)
3. 맵(Map)

배열(Array)


Go언어에서의 배열은 정적입니다. 즉, 고정된 배열 크기 안에 동일한 데이터를 연속적으로 저장할 수 있다는 뜻입니다. 배열의 선언은 "var 배열이름 [배열크기]자료형" 형식으로 합니다. Go언어에서는 배열의 크기는 자료형을 구성하는 한 요소입니다.
따라서, [3]int와 [5]int는 string과 float32처럼 타입 자체가 다른 것입니다. 배열이 선언되면 배열의 요소를 인덱스를 사용하여 읽거나 쓸 수 있습니다.

import "fmt"

func main() {
	var arr1 [5]int   //길이가 5인 int형 배열 arr1을 선언
	fmt.Println(arr1) //숫자를 선언하지 않고 출력해보기

	arr1 = [5]int{1, 2, 3, 4, 5}        //배열 초기화
	fmt.Println(arr1, arr1[0], arr1[4]) //배열 전체와 인덱스에 저장된 값들 출력해보기

	arr2 := [4]int{4, 5, 6, 7} //:= 를 이용해 선언
	arr2[0] = 32               //인덱스를 이용해 값을 초기화
	fmt.Println(arr2)          //arr2 전체 출력해보기

	var arr3 = [...]int{9, 8, 7, 6} //[...]을 이용한 배열 크기 자동 설정
	fmt.Println(arr3, len(arr3))    //arr3 전체와  배열 크기 출력해보기
}

다차원 배열


다른 언어들과 마찬가지로 Go언어도 다차원 배열을 지원합니다.

var multiArray [2][3][4]int //3차원 배열 선언
multiArray[1][1][2] = 10  // 인덱스를 이용한 값 초기화

슬라이스(Slice)


Slice(슬라이스)는 배열과 다르게 고정된 크기를 미리 지정하지 않고 이후에 필요에 따라 크기를 동적으로 변경할 수 있고, 부분 발췌가 가능합니다. 그래서 슬라이스는 지금까지 배운 자료형과 내부적인 구조가 다르기때문에 선언 및 초기화를 할 때 주의해야합니다. 슬라이스를 var a []int와 같이 선언한다면 배열의 일부분을 가리키는 포인터를 만듭니다. 슬라이스의 정보만 있는 배열만 생성되고, 실질적으로 어떠한 변수가 들어갈 공간(메모리)은 생성되지 않습니다.
따라서 슬라이스의 초기 값을 지정하지 않고 선언만 한다면 'Nil silce'가 됩니다. 이것은 크기도 용량도 없는 상태를 의미합니다.
사진 기본적으로 슬라이스는 아무런 값도 초기화하지 않아도 배열의 위치를 가리키는 ptr과 배열의 길이인 len, 전체크기인 cap 메모리를 가지고 있습니다.
그렇기 때문에 슬라이스를 var a []int와 같이 선언을 할 때는 주로 var a []int = []int{1, 2, 3, 4}같이 선언과 동시에 값을 초기화할 때만 사용합니다.
사진 슬라이스는 참조 타입이기 때문에 슬라이스를 복사해온다는 것은 사실 같은 주소를 참조한다는 것과 같은 말입니다.
예를 들어, 슬라이스는 다른 슬라이스를 부분 복사할 수 있는 기능이 있는데 슬라이스 a를 부분 복제하려고 하는 슬라이스 l은 l = a[2:5]를 입력함으로써 슬라이스 a의 인덱스2 요소부터 4요소까지 참조합니다.

package main

import "fmt"

func main() {
	var a []int        //슬라이스 변수 선언 아무것도 초기화 되지 않은 상태
	a = []int{1, 2, 3} //슬라이스에 리터럴값 지정

	a[1] = 10 //값이 할당되어 메모리가 생겼기 때문에 이렇게 접근 가능

	fmt.Println(a)

	var b []int //nil slice 선언

	if b == nil {
		fmt.Println("용량이", cap(b), "길이가", len(b), " Nil Slice입니다.")
	}
}

make() 함수를 이용한 슬라이스 선언


슬라이스를 생성하는 또 다른 방법으로는 Go언어내장 함수인 make() 함수를 이용한 선언입니다. 이 함수는 개발자가 슬라이스를 생성함과 동시에 슬라이스의 길이(len), 슬라이스의 용량(cap)을 저장할 수 있습니다. make() 함수"make(슬라이스 타입, 슬라이스 길이, [슬라이스의 용량])" 형태로 선언합니다.
여기서 용량(Capacity)은 생략해서 선언할 수 있습니다. 용량을 생략한다면 슬라이스의 길이와 똑같은 값으로 선언됩니다. 이렇게 make() 함수를 이용해 선언한다면 비로소 모든 요소가 0인 슬라이스를 만들게 됩니다.

길이 : 초기화된 슬라이스의 요소 개수 즉, 슬라이스에 5개의 값이 초기화된다면 길이는 5가 됩니다. 그 후에 값을 추가하거나 삭제한다면 그만큼 길이가 바뀌게 됩니다. "len(컬렉션이름)"으로 길이를 알 수 있습니다.

용량 : 슬라이스는 배열의 길이가 동적으로 늘어날 수 있기 때문에 길이와 용량을 구분합니다. 선언한 슬라이스의 용량이 25인데 101개의 값을 초기화하기 위해서는 125의 용량이 필요하게됩니다. 이러한 방식으로 메모리를 관리하는 것입니다. 용량은 "cap(컬렉션이름)"으로 용량을 알 수 있습니다.

그리고 주의해야할 점make() 함수를 이용해 슬라이스의 메모리를 할당하고 난 후에 []int{1,2,3,4}와 같은 식으로 입력하여 값을 초기화하면 새로운 메모리를 할당하면서 그 전의 값은 없어집니다. 어느 부분에서든 동일하게 적용되는 당연한 것입니다. 기존의 메모리를 사용하고 값을 추가하기 위해서는 아래에서 배우는 append() 함수를 사용해야합니다.

package main

import "fmt"

func main() {
	s := make([]int, 0, 3) // len=0, cap=3 인 슬라이스 선언

	for i := 1; i <= 10; i++ { // 1부터 차례대로 한 요소씩 추가
		s = append(s, i)

		fmt.Println(len(s), cap(s)) // 슬라이스 길이와 용량 확인
	}

	fmt.Println(s) // 최종 슬라이스 출력
}

슬라이스 추가, 병합, 복사


append() 함수를 이용해서 슬라이스에 데이터를 추가할 수 있습니다. 슬라이스 용량이 남아있는 경우에는 그 용량 내에서 슬라이스의 길이를 변경하여 데이터를 추가하고, 용량이 초과하는 경우에는 설정한 용량만큼 새로운 배열을 생성하고 기존 배열 값들을 모두 새 배열에 복제한 후 다시 슬라이스를 할당하는 방식입니다. 그리고 데이터를 추가할수 있을 뿐만이 아니라 슬라이스에 슬라이스를 추가해서 붙일 수 있습니다. 여기서 슬라이스에 슬라이스를 추가하기 위해 주의할 점은 추가하는 슬라이스 뒤에 "..."을 입력해야 한다는 것입니다. …은 슬라이스의 모든 요소들의 집합을 표현하는 것으로 아래 예제의 “sliceB…“은 슬라이스의 요소 집합인 {4, 5, 6}으로 치환되는 것입니다. 따라서 사실상 슬라이스에 슬라이스를 추가하는 것이 아니라, sliceA에 {4, 5, 6}이라는 요소들이 추가되는 것입니다.

package main
 
import "fmt"
 
func main() {
    sliceA := []int{1, 2, 3}
    sliceB := []int{4, 5, 6}
 
    sliceA = append(sliceA, sliceB...)
    //sliceA = append(sliceA, 4, 5, 6)
 
    fmt.Println(sliceA) // [1 2 3 4 5 6] 출력
}

copy() 함수를 이용해 한 슬라이스를 다른 슬라이스로 복사할 수 있습니다. copy() 함수는 "copy(붙여넣을 슬라이스, 복사할 슬라이스)" 형식으로 사용합니다. 당연히 복사할 슬라이스와 붙여넣을 슬라이스 모두 선언이 선행돼야 합니다.

package main

import "fmt"

func main() {
	sliceA := []int{0, 1, 2}
	sliceB := make([]int, len(sliceA), cap(sliceA)*2) //sliceA에 2배 용량인 슬라이스 선언

	copy(sliceB, sliceA)                              //A를 B에 붙여넣는다

	fmt.Println(sliceB)                               // [0 1 2 ] 출력
	println(len(sliceB), cap(sliceB))                 // 3, 6 출력
}

그리고 슬라이스의 부분만 잘라서 복사할 수도 있습니다. 이때 "붙여넣을 슬라이스 := 복사할 슬라이스[복사할 첫 인덱스:복사할 마지막 인덱스+1]"이라고 하면 잘라서 복사할 수 있습니다.(‘:=’ 용법을 이용해 바로 선언과 동시에 값을 저장함)

package main

import "fmt"

func main() {
	c := make([]int, 0, 3) //용량이 3이고 길이가0인 정수형 슬라이스 선언
	c = append(c, 1, 2, 3, 4, 5, 6, 7)
	fmt.Println(len(c), cap(c))

	l := c[1:3] //인덱스 1요소부터 2요소까지 복사
	fmt.Println(l)

	l = c[2:] //인덱스 2요소부터 끝까지 복사
	fmt.Println(l)

	l[0] = 6

	fmt.Println(c) //슬라이스 l의 값을 바꿨는데 c의 값도 바뀜
	//값을 복사해온 것이 아니라 기존 슬라이스 주솟값을 참조
}
cap 규칙
1. 3의 공간을 만든다.
2. 7개 요소가 들어온다.
3. 3보다 많은 수가 들어오니 우선 2배로 늘린다. (cap = 6)
4. 2배로 늘렸는데도 요소 수가 남으면 2배 늘린 cap에 2를 더한다. (cap = 8)
5. 요소 수인 7이 8을 넘지 않으니 여기서 멈춘다.
6. 만약 요소 수가 8을 넘으면 4를 반복한다. (cap = 10) 
이는 golang에서 이미 정해진 규칙입니다. (https://github.com/golang/go/blob/master/src/runtime/slice.go?fbclid=IwAR2RWbrxXHVKtWGUm3LFzYmQOT1bFP7hEe530ef7xq5pSLwK6S_9O6Kj8OA)에 구현된 코드가 나와있긴 하지만 지금 읽기는 좀 어려우실 거에요 참고만해주세요!

맵(Map)


key 값과 그에 해당하는 value 값을 매핑해 저장하는 'Hash table'입니다. 이것을 Go언어에서도 제공하는데 바로 'map(맵)'이라는 컬렉션입니다. 이는 Ruby에서의 hash와 python에서의 dicts와 같은 기능을 합니다. map은 "key : value" 형식으로 값을 매핑해서 저장합니다. 그리고 슬라이스와 맵의 공통점은 두 컬랙션 모두 값을 직접적으로 저장하는 것이 아닌 '참조 타입(Reference type)'이라는 점입니다. 따라서 선언과 값을 초기화 함에 있어서 슬라이스와 같은 방법과 이유를 가진다고 생각하면 됩니다(참조 타입의 선언 방법은 Slice 참고). 맵의 선언은 "var 맵이름 map[key자료형]value자료형" 형식으로 합니다. 이렇게 선언만 하고 값을 초기화하지 않았다면 'Nil map'이 됩니다.

package main

import "fmt"

func main() {
	var a map[int]string

	if a == nil {
		fmt.Println("nil map")
	}

	var m = map[string]string{ //key:value, 형식으로 초기화한다
		"apple":  "red",
		"grape":  "purple",
		"banana": "yellow",
	}

	fmt.Println(m, "\nm의 길이는", len(m))
}

map 변수의 추가, 갱신, 삭제


맵 컬렉션메모리가 할당(make() 함수 혹은 {}를 사용한 값 초기화)됐으면 값을 추가, 갱신, 삭제할 수 있습니다. 맵에서의 key값은 특별(unique)하기 때문에 "맵이름[key] = value" 형식으로 값을 추가할 수 있습니다. 주의할 점은 이미 있는 key값에 다시 다른 value값을 저장한다면 최근 저장한 값으로 갱신됩니다. 그리고 delete() 함수를 이용해 저장된 값을 삭제할 수 있습니다. "delete(맵이름, key)" 형식을 입력하면 key값에 해당되는 value값이 같이 삭제됩니다.

package main

import "fmt"

func main() {
	//지역번호와 지역 저장
	var m = make(map[string]string)

	m["02"] = "서울특별시"
	m["031"] = "경기도"
	m["032"] = "충청남도"
	m["053"] = "대구광역시"

	fmt.Println(m)

	//동일한 key값으로 value값을 저장하면 갱신이 된다
	m["032"] = "인천"

	fmt.Println(m)

	//m에 있는 "031"key의 value와 함께 삭제
	delete(m, "031")

	fmt.Println(m)
}

Map의 key 체크와 value 읽기


'컬렉션이름[인덱스]'의 사용에 있어서 다른 두 컬렉션차이점이 있습니다. ‘맵이름[key]’는 key에 저장돼 있는 value 값을 반환할 뿐만 아니라, 해당 키에 값이 존재하는지 안 하는지 즉, 사용하고 있는 key 값인지 아닌지 판별해주는 true/false 값도 반환합니다.
위에 예시에서 m[“031”]은 “경기도”와 “true”를 반환합니다. 하지만 이 용법에는 몇 가지 주의할 점이 있습니다.

1.콘솔 출력 함수에 "맵이름[key]"을 바로 입력할 때는 key 값에 해당되는 value 값만 출력됩니다. 
2.value 값과 true/false 값을 반환받기 위해서는 변수 두 개를 선언한 후에 각각 할당받아야 합니다. 예를 들어 val, exist := 맵이름[key]형식으로 입력해야만 val에는 value 값이, exist에는 true/false 값이 초기화됩니다.
3. value 값만 반환받고 싶다면 변수 한 개만 선언해 할당받으면 됩니다. 예를 들어 val := 맵이름[key]형식으로 입력하면 val에 value 값이 초기화됩니다. 꼭 두 개의 값을 반환하는 것은 아닙니다.
4.true/false 값만 반환받고 싶다면 "_, bool변수"형식으로 선언해 할당받으면 됩니다. 예를 들어 _, exist := 맵이름[key]형식으로 입력하면 exist에 true/false 값이 초기화됩니다. 왜냐하면 '맵이름[key]'는 꼭 'value, true/false' 순서로 반환하기 때문입니다. 따라서 앞에 value 값을 받는 인자를 '_,' 용법을 써 생략한다면 true/false 값만 반환받을 수 있는 것입니다. 

그리고 for key, val := range 맵이름으로 접근할 수 있습니다.

package main

import "fmt"

func main() {
	//지역번호와 지역 저장
	var m = make(map[string]string)

	m["02"] = "서울특별시"
	m["031"] = "경기도"
	m["032"] = "인천"
	m["053"] = "대구광역시"

	fmt.Println(m["032"])
	fmt.Println(m["042"], "빈 칸입니다.") //string형태로 존재하지 않는 key값은 ""가 출력된다

	val, exist := m["02"] //존재하는 key
	fmt.Println(val, exist)

	val, exist = m["042"] //존재하지 않는 key
	fmt.Println(val, exist)

	val = m["053"] //value 값만 반환
	fmt.Println(val)

	_, exist = m["053"] //true/false 값만 반환
	fmt.Println(exist)

	//맵도 똑같이 len() 함수를 사용할 수 있다 하지만 cap() 함수는 사용할 수 없다
	fmt.Println(len(m))
}

© 2022. All rights reserved. 신동민의 블로그