Featured image of post Golang: 5. 컬렉션

Golang: 5. 컬렉션

배열 Array

배열은 연속적인 메모리 공간에 동일한 타입의 데이터를 순차적으로 저장하는 자료구조이다.

배열의 선언은 var 변수명 [배열크기] 데이터타입 과 같이 한다. Go에서 배열크기는 Type을 구성하는 한 요소이다. 따라서, [3]int와 [5]int는 서로 다른 타입으로 인식된다.

1
2
3
4
5
6
7
8
9
package main
 
func main() {
    var a [3]int  //정수형 3개 요소를 갖는 배열 a 선언
    a[0] = 1
    a[1] = 2
    a[2] = 3
    println(a[1]) // 2 출력
}

배열 초기화

배열을 정의할 때, 초기값을 설정할 수 있다. 초기값은 [배열크기] 데이터타입 {초기값0, 초기값1 ...} 로 할당한다.

초기화 과정에서 […]를 사용하여 배열 크기를 생략하면 자동으로 초기화 요소 개수만큼 배열 크기가 정해진다.

1
2
var a1 = [3]int{1, 2, 3}
var a3 = [...]int{1, 2, 3} //배열크기 자동으로

다차원 배열

1
2
var multiArray [3][4][5]int  // 정의
multiArray[0][1][2] = 10

다차원 배열 초기화

1
2
3
4
5
6
7
func main() {
    var a = [2][3]int{
        {1, 2, 3},
        {4, 5, 6},  //끝에 콤마 추가
    }
    println(a[1][2])
}

슬라이스 Slice

Go 배열은 고정된 크기 안에 동일한 타입의 데이터를 연속적으로 저장하지만, 배열의 크기를 동적으로 증가시키거나 부분 배열을 추출하는 등의 기능은 없다.

Go 슬라이스는 내부적으로 배열에 기초하여 만들어졌지만 편리하고 유용한 기능들을 제공한다.

  • 고정된 크기를 지정하지 않을 수 있음
  • 크기를 동적으로 변경할 수 있음
  • 부분 배열 추출 가능 등

Go 슬라이스 선언은 배열을 선언하듯이 var v []T 처럼 하는데 배열과 달리 크기는 지정하지 않는다.

1
2
3
4
5
6
7
8
9
package main
import "fmt"
 
func main() {
    var a []int        //슬라이스 변수 선언
    a = []int{1, 2, 3} //슬라이스에 리터럴값 지정
    a[1] = 10
    fmt.Println(a)     // [1, 10, 3]출력
}

make()

슬라이스를 생성하는 다른 방법으로 내장함수 make()를 이용할 수 있다.

make함수로 슬라이스를 생성하면 슬라이스 길이(length), 용량(Capacity)을 임의로 지정할 수 있는 장점이 있다.

1
2
3
4
func main() {
    s := make([]int, 5, 10)
    println(len(s), cap(s)) // len 5, cap 10
}

→ 슬라이스의 길이는 len(), 용량은 cap()을 써서 확인할 수 있다.

1
2
3
4
5
6
7
8
func main() {
    var s []int
 
    if s == nil {
        println("Nil Slice")
    }
    println(len(s), cap(s)) // 모두 0
}

make함수로 슬라이스를 생성하면 모든 요소가 Zero value인 슬라이스를 만들게 된다. 또한 슬라이스에 별도의 길이와 용량을 지정하지 않으면, 기본적으로 길이와 용량이 0인 슬라이스를 만드는데 이를 Nill Slice 라고 하며, nil 과 비교하면 참을 반환한다.

#$# 부분 슬라이스

슬라이스에서 일부를 발췌하여 부분 슬라이스를 만들 수 있다. 부분 슬라이스는 슬라이스[시작인덱스:마지막인덱스] 형식으로 만든다.

시작 인덱스는 inclusive이며 마지막 인덱스는 Exclusive이다(파이썬과 동일함)

1
2
3
4
5
6
7
8
package main
import "fmt"
 
func main() {
    s := []int{0, 1, 2, 3, 4, 5}
    s = s[2:5]  
    fmt.Println(s) //2,3,4 출력
}

부분 슬라이스에서 인덱스는 생략 가능하다.

  • 처음 인덱스 생략: 0 자동 대입
  • 마지막 인덱스 생략: 슬라이스 길이 자동대입

따라서 모두 생략하면 전체를 가져온다.

1
2
3
4
s := []int{0, 1, 2, 3, 4, 5}
s = s[2:5]     // 2, 3, 4
s = s[1:]      // 3, 4
fmt.Println(s) // 3, 4 출력

슬라이스 추가

배열은 고정된 크기로 그 크기 이상의 데이터를 임의로 추가할 수 없지만, 슬라이스는 자유롭게 새로운 요소를 추가할 수 있다.

슬라이스에 새로운 요소를 추가하려면 내장함수 append()를 사용한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func main() {
    s := []int{0, 1}
 
    // 하나 확장
    s = append(s, 2)       // 0, 1, 2
    // 복수 요소들 확장
    s = append(s, 3, 4, 5) // 0,1,2,3,4,5
 
    fmt.Println(s)
}

append 동작 과정

  • 슬라이스 용량이 남아있는 경우
    • 슬라이스의 길이를 변경하여 데이터를 추가
  • 슬라이스 용량을 초과하는 경우
    • 현재 용량의 2배에 해당하는 새로운 Underlying array를 생성하고 기존 배열 값들을 모두 새 배열에 복제한 후 다시 슬라이스를 할당.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main
 
import "fmt"
 
func main() {
    // len=0, cap=3 인 슬라이스
    sliceA := make([]int, 0, 3)
 
    // 계속 한 요소씩 추가
    for i := 1; i <= 15; i++ {
        sliceA = append(sliceA, i)
        // 슬라이스 길이와 용량 확인
        fmt.Println(len(sliceA), cap(sliceA))
    }
 
    fmt.Println(sliceA) // 1 부터 15 까지 숫자 출력 
}

병합

한 슬라이스를 다른 슬라이스 뒤에 병합하기 위해서는 append()ellipsis(...)를 사용한다.

ellipsis(...) 은 파이썬 asterisk(*)와 같이 동작한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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()를 사용하여 한 슬라이스를 다른 슬라이스로 복사할 수도 있다.

1
2
3
4
5
6
7
func main() {
    source := []int{0, 1, 2}
    target := make([]int, len(source), cap(source)*2)
    copy(target, source)
    fmt.Println(target)  // [0 1 2 ] 출력
    println(len(target), cap(target)) // 3, 6 출력
}

슬라이스 내부구조

슬라이스는 내부적으로 사용하는 배열의 부분 영역인 세그먼트에 대한 메타 정보를 가지고 있다. 슬라이스는 크게 3개의 필드로 구성되어 있다.

  • 내부적으로 사용하는 배열에대한 포인터
  • 세그먼트 길이
  • 세그먼트 최대 용량

처음 슬라이스가 생성될 때, 길이와 용량이 지정되었다면, 내부적으로 용량만큼 크기의 배열을 생성하고, 슬라이스 첫번째 필드에 그 배열의 처음 메모리 위치를 지정한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

const limit = 10

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

	subSlice := slice[2:5]

	subSlice[0] = 20
	subSlice[1] = 30
	subSlice[2] = 40

	for _, v := range slice {
		println(v)
	}
}
1
2
3
4
5
6
0
1
20
30
40
5

서브 슬라이스를 만들면 슬라이스 생성시와 마찬가지로 지정 인덱스만큼의 길이와 용량을 설정하게 되며, 배열 포인터는 시작 슬라이스 위치로 초기화 된다.

위의 예시의 서브 슬라이스의 길이는 3 용량은 4로 만들어진다.

따라서 서브 슬라이스의 값을 변경하면 원본 슬라이스의 값도 변경된다.

Map

선언

Map은 키에 대응하는 값을 신속히 찾는 해시테이블을 구현한 자료구조이다. Go 언어는 Map 타입을 내장하고 있는데, map[key 타입]값타입 로 선언할 수 있다.

1
var idMap map[int]string

이때 선언된 idMap은 레퍼런스 타입이므로 nil 값을 갖으며, 이를 Nil Map이라고 부른다. Nil map은 어떤 데이터를 쓸 수 없는데, map을 초기화하기 위해 make()함수를 사용할 수 있다.

초기화 make()

1
idMap = make(map[int]string)

make() 함수의 첫번째 파라미터로 map 키워드와 [키타입]값타입을 지정하는데, 이때 make()함수는 해시테이블 자료구조를 메모리에 생성하고 그 메모리를 가리키는 map value를 리턴한다.

→ map value는 내부적으로 runtime.hmap 구조체를 가리키는 포인터이다.

따라서 idMap 변수는 이 해시테이블을 가리키는 map을 가리키게 된다.

초기화 - 리터럴

map은 make() 함수를 써서 초기화할 수도 있지만, 리터럴을 사용해 초기화할 수도 있다. 리터럴 초기화는 map[key타입]value타입 {key:value} 와 같이 Map 타입 뒤 중괄호 안에 ‘키:값’들을 결거하면 된다.

1
2
3
4
5
ticker := map[string]string{
    "GOOG": "Google Inc",
    "MSFT": "Microsoft",
    "FB": "FaceBook",
}

Map 사용

처음 mapmake() 함수에 의해 초기화 되었을 때는, 아무 데이터가 없는 상태이다. 이때 새로운 데이터를 추가하기 위해서는 map변수[키] = 값 처럼 해당 키에 그 값을 할당하면 된다.

만약 키 값이 이미 존재한다면 추가 대신 값만 갱신한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

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

	m = make(map[int]string)

	m[901] = "Apple"
	m[134] = "Grape"
	m[777] = "Tomato"

	str := m[134]
	println(str)

	noData := m[999]
	println(noData)

	println(m[777])
	delete(m, 777)
	println(m[777])
}

만약 map안에 찾는 키가 존재하지 않는다면 reference 타입인 경우 nil, value 타입인 경우 zero를 리턴한다.

map에서 특정 키와 그 값을 삭제하기 위해서는 delete() 함수를 이용한다.

Map 키 체크

map을 사용하는 경우 종종 map안에 특정 키가 존재하는지를 체크할 필요가 있다. 이를 위해 go에선 “map 변수[키]” 읽기를 수행할 때 2개 값을 반환한다.

  1. 키에 상응하는 값
  2. 키 존재 여부 (bool)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main
 
func main() {
    tickers := map[string]string{
        "GOOG": "Google Inc",
        "MSFT": "Microsoft",
        "FB":   "FaceBook",
        "AMZN": "Amazon",
    }
 
    // map 키 체크
    val, exists := tickers["MSFT"]
    if !exists {
        println("No MSFT ticker")
    }
}

for 루프를 사용한 Map 열거

Map이 가진 모든 요소들을 출력하기 위해, for range 루프를 사용할 수 있다. Map 컬렉션에 for range를 사용하면, Map 키와 Map 값 2개 데이터를 반환한다.