배열, 슬라이스 그리고 맵

Array, slice and map

Go의 built-in 자료구조인 배열, 슬라이스, 에 대해 좀 더 자세히 살펴보자.

크기

배열, 슬라이스, 맵은 모두 복수의 요소를 저장할 수 있다. 따라서 몇 개의 요소를 가지고 있는지가 중요하다. 세 타입은 모두 len() 함수를 이용해 포함하고 있는 요소의 개수를 확인할 수 있다.

package main

import "fmt"

func main() {
	a := [3]int {0, 1, 2}
	s := []int {0, 1, 2}
	m := map[string]int {"A":0, "B":1, "C":2}
	fmt.Println(len(a))
	fmt.Println(len(s))
	fmt.Println(len(m))	
}

위 예는 다음을 출력한다.

3
3
3

슬라이스나 맵에 비해 배열은 이미 크기를 지정하고 있다는 점에 주목할 필요가 있다. 즉, 배열 타입의 변수는 값을 넣지 않아도 이미 생성 시점에 크기가 정해져 있다.

package main

import "fmt"

func main() {
	var a [3]int
	var s []int
	var m map[string]int
	fmt.Println(len(a))
	fmt.Println(len(s))
	fmt.Println(len(m))
}

위 예는 다음을 출력한다.

3
0
0

슬라이스와 맵은 동적으로 조절이 가능한 반면 배열은 정적 타입이다. 따라서 슬라이스와 맵은 크기만으로 몇 개의 요소가 저장돼 있는지 알 수 있지만 배열은 불가능하다.

package main

import "fmt"

func main() {
	var s = make([]int, 0)
	var m = make(map[string]int)
	fmt.Println(len(s))
	fmt.Println(len(m))
	s = append(s, 1)
	m["A"] = 1
	fmt.Println(len(s))
	fmt.Println(len(m))
}
0
0
1
1

복사 또는 참조

배열을 다른 배열 변수에 대입하면 요소값이 복사된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import "fmt"

func main() {
	a1 := [3]int {1, 2, 3}
	a2 := a1
	a2[2] = 4
	fmt.Println(a1[2], a2[2])
}
3 4

위 예는 7라인에서 a2 변수에 a1을 대입하면서 각 요소들이 모두 복사되기 때문에 a2의 세 번째 요소값을 4로 변경하더라도—8라인— a1에 영향을 주지 않는다.

반면 슬라이스나 맵은 참조형이다.

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

import "fmt"

func main() {
	s1 := []int {1, 2, 3}
	s2 := s1
	s2[2] = 4
	fmt.Println(s1[2], s2[2])
	

	m1 := map[string]int {"A":1, "B":2, "C":3}
	m2 := m1
	m2["C"] = 4
	fmt.Println(m1["C"], m2["C"])
}
4 4
4 4

슬라이스는 슬라이싱을 이용해 일부 요소만을 참조할 수도 있는데 이 경우 주의할 점이 있다. 다음 예를 보자.

package main

import "fmt"

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

s2는 슬라이싱을 이용해 s1의 첫번째 요소만을 참조하고 있다. 따라서 이 경우 s2[0]의 값을 변경하면 s1[0]의 값 역시 변경된다. 또, s2의 두 번째 요소—s2[1]—에 접근하면 runtime error가 발생한다. 그런데 append()를 이용해 s2에 두 번째 요소값을 추가하면 어떻게 될까?

package main

import "fmt"

func main() {
	s1 := []int {1, 2, 3}
	s2 := s1[0:1]
	s2 = append(s2, 4)
	fmt.Println(s1[0], s1[1], s1[2])
	fmt.Println(s2[0], s2[1])
}
1 4 3
1 4

위와 같이 슬라이싱으로 일부 요소값만 참조했더라도 append()를 이용해 값을 추가하면 동시에 변경되는 것을 확인할 수 있다. 어떻게 쓰더라도 슬라이스는 참조형이다.

반복

여러 개의 요소를 저장했으니 하나씩 반복해 저장된 값을 확인할 방법이 필요하다.

배열과 슬라이스는 다음과 같이 값을 확인할 수 있다.

package main

import "fmt"

func main() {
	a := [3]int {0, 1, 2}
	s := []int {0, 1, 2}
	for i := 0; i < len(a); i++ {
		fmt.Println(a[i])
	}
	for j := 0; j < len(s); j++ {
		fmt.Println(s[j])
	}
}

이 방식은 요소를 저장하기 위한 키—0부터 (길이 - 1)까지—를 알기 때문에 가능하다. 그런데 키를 모르는 맵의 경우에는 어떻게 구현할 수 있을까?

맵에서 키 추출

키를 모르는 것이 문제라면 어떻게든 알아내면 된다. reflect 패키지를 사용하면 맵에서 키를 추출할 수 있다.

package main

import (
    "fmt"
    "reflect"
)

func main() {
    m := map[string]int {"a":0, "b":1, "c":2}
    keys := reflect.ValueOf(m).MapKeys()
	fmt.Println(keys)
}
[a b c]

하지만 이는 미련한 방법으로 꼭 키만 별도로 추출할 필요가 있는 경우가 아니라면 사용하지 않는다.

range

for 반복문을 다시 보자. 한 가지 확인하지 않은 용법이 있다. 바로 RangeClause 용법이다.

RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .

배열, 슬라이스, 맵에 대해 range 명령은 두 가지 값을 반환하는데 첫 번째가 키, 두 번째가 값이다.

// array
package main

import "fmt"

func main() {
    a := [3]int {1, 2, 3}
	for i, v := range a {
		fmt.Println(i, v)
	}
}
0 1
1 2
2 3
// slice
package main

import "fmt"

func main() {
    s := []int {1, 2, 3}
	for i, v := range s {
		fmt.Println(i, v)
	}
}
0 1
1 2
2 3
// map
package main

import "fmt"

func main() {
    m := map[string]int {"a":1, "b":2, "c":3}
	for i, v := range m {
		fmt.Println(i, v)
	}
}
b 2
c 3
a 1

맵은 순서가 보장되지 않는다는 점도 확인할 수 있다.

항상 키와 값 모두가 필요한 것은 아니다. 둘 중 하나가 필요없는 경우에는 blank identifier를 대신 사용할 수 있다.

package main

import "fmt"

func main() {
    m := map[string]int {"a":1, "b":2, "c":3}
	for _, v := range m {
		fmt.Println(v)
	}
}
2
3
1

값이 불필요한 경우에는 blank identifer를 사용하지 않고 그냥 생략해도 된다. 즉, 복수의 반환값이 있는 경우에는 앞에서부터 차례로 값을 넣고, 남은 것들은 무시된다.

package main

import "fmt"

func main() {
    m := map[string]int {"a":1, "b":2, "c":3}
	for i := range m {
		fmt.Println(i)
	}
}
a
b
c