Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

Contents

소개

해시 테이블(hash table)는 컴퓨터 과학에서 가장 많이 사용되는 자료구조 중 하나다. 많은 종류의 해시 테이블 구현이 있고, 이들 마다 기능이 다르긴 하지만 fast lookups, add, delete는 공통으로 가지고 있다. Go는 해시 테이블의 구현인 map 타입을 내장하고 있다.

선언과 초기화

Go map 타입은 다음과 같은 모습을 가진다.
map[KeyType]ValueType
KeyType은 비교가 가능한 어떤 타입이라도 사용할 수 있다. ValueType는 map을 포함한 어떠한 타입이라도 사용 할 수 있다.

string이 키이고 int가 값인 map 타입의 변수 m은 아래와 같이 선언한다.
var m map[string]int

Map 타입은 포인터나 슬라이스와 같은 레퍼런스(reference)타입이다. 다라서 m은 nil을 값으로 가질 수 있다. nil 맵은 empty map으로 읽기도 하지만 초기화 없이 사용하면 패닉(panic)을 발생한다. make 함수를 이용해서 초기화 후 사용해야 한다.
m = make(make[string]int)

make 함수는 해시 맵의 자료구조를 위한 공간을 할당하고 초기화 한후 포인터를 반환한다.

map 사용하기

Go의 map은 다른 언어들(pythin이나 ruby)의 맵과 비슷하게 사용할 수 있다. "router" 키에 66을 저장하는 방법이다.
m["route"] = 66

"route"키를 찾아서 그 값을 변수 i에 할당하는 코드다.
i := m["route"]

키가 없을 경우에는 해당 타입의 zero value를 반환한다. int의 zero value는 0이다.
j := m["root"]
// j = 0

len 함수를 이용해서 map의 아이템 갯수를 알 수 있다.
n := len(m)

delete함수로 map에 있는 특정 키를 지울 수 있다.
delete(m, "route")
delete는 아무 것도 반환하지 않는다. 지울 키를 찾지 못했다면 아무일도 일어나지 않는다.

키가 있는지 확인하려면 테스트 값도 함께 할당하면 된다.
i, ok := m["route"]
i에는 키 "route"의 값이 할당된다. 만약 키가 존재하지 않는다면 ok에는 false가 할당되고, 존재한다면 true가 할당된다.

range 키워드를 이용해서 map을 순환할 수 있다.
for key, value := range m {
	fmt.Println("Key:",key, "Value:", value)
}

map 리터럴을 이용해서 데이터로 초기화 하는 방법이다.
m := map[string]int {
	"rsc": 3711,
	"r":   1222,
	"gri": 8312,
	"adg": 912,
}

비어있는 맵을 초기화 할 수도 있다.
m := map[string]int{}

Key types

Map 키는 비교가 가능한(comparable type)어떤 타입이라도 사용할 수 있다. 언어 사양은 이를 정확하게 정의하고 있다. 대표적인 비교 가능한 타입으로는 boolean, numeric, string, pointer, channel, 인터페이스 그리고 이들 타입을 포함하고 있는 구조체와 배열들이다.

목록에서 슬라이스와 맵 함수들은 빠져있는데, "=="를 이용해서 비교를 할 수 없는 타입들이다. 이들 타입은 map의 키로 사용할 수 없다.

String이나 int등의 기본 타입들은 키로 사용해도 문제 없을 것 같은데, 구조체는 어떻게 가능한지 좀 궁금해진다. 예를들어 국가별로 페이지뷰(PV)를 확인하고 싶다면, 맵의 맵을 이용할 수 있을 것이다.
hits := make(map[string]map[string]int)
예를 들어 kr 문서에 대한 PV를 가져오는 코드는 아래와 같을 것이다.
n := hits["/doc/"]["kr"]
이 경우 앞쪽에 있는 맵이 존재하는지 확인해서 추가하는 코드를 만들어야 한다.
package main

import (
	"fmt"
)

func add(m map[string]map[string]int, path, country string) {
	mm, ok := m[path]
	if !ok {
		mm = make(map[string]int)
		m[path] = mm
	}
	mm[country]++
}
func main() {
	hits := make(map[string]map[string]int)
	add(hits, "/doc", "kr")
}

구조체를 이용하면 하나의 맵만으로 간단하게 구현할 수 있다.
type Key struct {
	Path, Country string
}
hits := make(map[Key]int)
국가별 PV 코드를 구조체의 맵을 이용해서 다시 만들었다.
package main

import (
	"fmt"
)

type Key struct {
	Path, Country string
}

func main() {
	hits := make(map[Key]int)
	hits[Key{"/doc/", "kr"}]++

	fmt.Println(hits[Key{"/doc/", "kr"}])
}

동시성

Map은 여러개의 고루틴들이 안전하게 접근하기 위한 안전장치를 가지고 있지 않다. 따라서 동시에 맵을 읽고 쓸때, 어떤 일이 일어날지 알 수 없다. 하나의 맵을 두 개 이상의 고루틴이 사용하게 하려면, sync.RWMutex 등을 이용해서 직접 동기화를 구현해야 한다.
package main

import (
	"fmt"
	"sync"
)

var counter = struct {
	sync.RWMutex
	m map[string]int
}{m: make(map[string]int)}

func main() {
	counter.RLock()
	counter.m["mykey"] = 1
	counter.RUnlock()

	counter.RLock()
	n := counter.m["mykey"]
	counter.RUnlock()
	fmt.Println(n)
}

Iteration order

range로 맵을 순환할 때, 다음 값이 뭐가 올지 예상 할 수 없다. 이번 순환 값과 다음번 순환값이 다를 수 있다. 안정적으로 맵을 순환하길 원한다면, 키를 분리해서 정렬 한 다음, key를 이용해서 출력 해야 한다.
package main

import (
	"fmt"
	"sort"
)

func main() {
	var m map[int]string
	var keys []int
	m = make(map[int]string)
	m[2] = "2"
	m[1] = "1"
	m[3] = "3"

	for k := range m {
		keys = append(keys, k)
	}
	sort.Ints(keys)

	for _, k := range keys {
		fmt.Println("Key: ", k, "Value:", m[k])
	}
}

참고