Go에서 map은 해시 테이블을 나타내는 내장 데이터 타입이에요. 다른 프로그래밍 언어에서는 map을 dict, 연관 배열, 또는 키/값 저장소라고 부르기도 하죠. 이런 개념이 낯설다면 map을 사전처럼 생각하면 돼요. 사전에서 모든 단어는 key이고, 그 단어의 뜻풀이는 map의 element예요.
시작하기 전에,
map을 더 깊이 파고들고 싶다면 map에 대한 Go 명세와 map에 대한 Go 블로그를 참고해 보세요.
문법적으로 map은 이렇게 생겼어요:
map[KeyType]ElementType
KeyType은 어떤 비교 가능한 타입이든 될 수 있고, ElementType은 Go에서 유효한 타입이라면 무엇이든 될 수 있어요. 즉, 기본형 변수부터 슬라이스까지 뭐든 저장할 수 있다는 뜻이죠.
Go의 map은 순서가 없다는 점을 꼭 기억해 두세요. map을 순회하면서 원소를 출력해 보면, 원소들이 무작위 순서로 출력되는 걸 보고 깜짝 놀랄지도 몰라요. (궁금하면 한번 해봐요.)
각 키가 유일하다는 점도 중요해요. 같은 키에 값을 두 번 할당하면 그 키에 해당하는 값이 덮어써진다는 뜻이거든요.
map은 참조 타입이에요. 그래서 map을 이리저리 넘겨도 Go가 map 전체를 복사하지 않아요. 대신 map의 포인터를 복사하는데, 덕분에 map을 함수나 변수에 넘기는 비용이 저렴하죠. 초기화하지 않은 map의 값은 nil이에요.
map은 다음과 같이 정의할 수 있어요. (이렇게 정의한 map을 nil map이라고도 불러요.)
var foo map[string]int
map을 초기화하려면 이렇게 하면 돼요:
// With map literal
foo := map[string]int{}
또는
// or with make function
foo := make(map[string]int)
nil map은 초기화된 map과 달라요. nil map에 값을 쓰면 런타임 오류가 발생해요
panic: assignment to entry in nil map따라서 map을 사용하기 전에 초기화하는 것이 중요해요
map으로 할 수 있는 몇 가지 연산을 살펴볼게요
// Add a value in a map with the `=` operator:
foo["bar"] = 42
// Here we update the element of `bar`
foo["bar"] = 73
// To retrieve a map value, you can use
baz := foo["bar"]
// To delete an item from a map, you can use
delete(foo, "bar")
map에 존재하지 않는 키의 값을 가져오려고 하면, 값 타입의 제로 값을 반환해요. 이 때문에 헷갈릴 수 있어요. 특히 ElementType의 기본값(예를 들어 int라면 0)이 유효한 값일 때 그렇죠. map에 어떤 키가 존재하는지 확인하려면 이렇게 하면 돼요:
value, exists := foo["baz"]
// If the key "baz" does not exist,
// value: 0; exists: false
앞서 봤듯이 map이 초기화됐는지 아닌지는 쉽게 알 수 있어요. nil과 비교하기만 하면 되니까요. 그런데 다른 map끼리는 어떨까요? map은 비교할 수 있는 타입이 아니라서 == 연산자를 쓸 수 없어요. 하지만 reflect 패키지에 DeepEqual이라는 구원투수가 있어요. 이렇게 쓰면 돼요:
import "reflect"
// ....
equal := reflect.DeepEqual(map[string]int{}, map[string]int{})
fmt.Println(equal)
// Output: true
그런데 잠깐, map이 비교할 수 있는 타입이 아니라면 왜 nil과는 비교할 수 있는 걸까요? 명세에서 이 경우만 예외를 두었기 때문이에요. 비교 가능한 타입 명세를 참고하세요.
마지막으로, 여러 고루틴에서 map에 값을 쓰려고 하면 레이스 디텍터가 작동해요. Go 문서와 Go 블로그를 참고하세요. 아니면 sync.Map이나 atomic, mutex를 사용해서 이 문제를 피할 수도 있어요.