맵 만들고 쓰기
맵 타입은 map[KeyType]ValueType으로 씁니다. 리터럴이나 make로 만든 뒤, 키로 읽고 쓰고 삭제합니다.
출력:
31
3
map[bob:26 cy:40]
2 0
여기서 두 가지 편의 기능이 보입니다. 없는 키를 읽으면 값 타입의 제로 값이 반환되므로(counts['z']는 0), m[k]++로 세는 코드가 준비 없이도 동작합니다. 그리고 fmt는 맵을 키 순서로 정렬해서 출력하는데, 디버깅에는 편리하지만 순회 순서에 대해서는 아무것도 알려 주지 않습니다.
make(map[K]V, n)은 선택적인 크기 힌트를 받습니다. 약 n개 항목을 위한 공간을 미리 할당합니다. 슬라이스와 달리 맵에는 다시 읽을 수 있는 용량이 없습니다.
키 존재 확인: comma-ok
없는 키는 제로 값으로 읽히므로 m[k] == 0으로는 "없음"과 "0으로 저장됨"을 구분할 수 없습니다. 두 값 형태를 쓰세요:
if v, ok := m[k]; ok { ... } 형태는 v와 ok의 스코프를 if로 한정합니다. Go 코드에서 가장 흔한 줄 중 하나입니다.
항목 삭제하기
delete(m, key)는 항목을 제거합니다. 없는 키를 삭제해도 아무 일도 일어나지 않고, nil 맵에서 삭제해도 마찬가지입니다. 맵 전체를 비우려면 Go 1.21에 추가된 clear(m)을 쓰세요. 할당된 맵을 유지하므로 재사용할 수 있습니다.
같은 맵을 range로 순회하는 동안 항목을 삭제해도 허용되고 안전합니다. 반복문이 도달하기 전에 삭제된 항목은 나오지 않습니다.
순회: 순서는 무작위다
for k, v := range m은 모든 항목을 정해지지 않은 순서로 한 번씩 방문합니다. 런타임이 일부러 시작 지점을 무작위로 정하므로, 같은 프로그램에서 같은 맵을 두 번 순회해도 순서가 다른 경우가 많습니다. 몇 번 실행해 보세요:
출력이 맵 순서에 의존하는 코드는 다음 실행을 기다리는 버그입니다. 출력된 맵 순회 결과를 비교하는 테스트가 전형적인 예입니다.
정렬된 키
키 순서로 맵을 방문하려면 키를 얻어 정렬한 뒤 맵을 인덱싱하세요. Go 1.23은 maps와 slices 패키지의 이터레이터로 이를 한 줄로 만들었습니다:
Go 1.22 이하의 표준 라이브러리에는 maps.Keys가 없었습니다. 같은 일을 하는 반복문은 다음과 같습니다:
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
maps의 다른 헬퍼: maps.Values, maps.Clone(얕은 복사), maps.Equal, maps.Copy(dst, src), maps.DeleteFunc.
키로 쓸 수 있는 타입
키는 ==로 비교할 수 있어야 합니다. 숫자, 문자열, 불리언, 포인터, 채널, 비교 가능한 타입의 배열, 모든 필드가 비교 가능한 구조체, 인터페이스 값이 해당됩니다. 슬라이스, 맵, 함수는 키가 될 수 없습니다.
map[[]int]bool{} // compile error: invalid map key type []int
map[[2]int]bool{} // fine: arrays are comparable
map[struct{ X, Y int }]string{} // fine: a struct key for a grid position
여러 값을 한꺼번에 키로 쓸 때는 문자열을 이어 붙이는 대신 구조체 키를 쓰는 것이 관용적입니다.
인터페이스 키는 동적 타입이 비교 불가능해도 컴파일되고, 런타임에 패닉이 납니다. map[any]int에 []int를 키로 저장하면 runtime error: hash of unhashable type []int로 실패합니다.
부동소수점 키도 동작하지만 NaN은 자기 자신과 같지 않으므로, NaN 키는 계속 삽입될 뿐 다시 읽을 수 없습니다. 실수 키는 피하세요.
구조체 맵
맵에 구조체를 담을 수는 있지만, 맵 값은 주소를 얻을 수 없으므로 맵에 저장된 구조체의 필드에 대입할 수 없습니다.
항목이 작고 통째로 교체된다면 값을 고르세요. 필드를 자주 갱신하거나 같은 레코드를 여러 곳에서 공유한다면 포인터를 고르세요. 포인터라면 없는 키가 nil을 반환하므로 ptrs["nope"].Score에서 패닉이 납니다.
슬라이스 맵에 append할 때도 같은 방식으로 동작합니다. 없는 키는 nil 슬라이스를 주고 append는 nil을 처리하므로 groups[k] = append(groups[k], v)에는 초기화가 필요 없습니다.
맵은 참조처럼 동작한다
맵 값은 공유 데이터를 참조합니다. 맵을 대입하거나 함수에 넘겨도 항목은 복사되지 않으며, 두 변수가 같은 맵을 봅니다.
그래서 append하는 슬라이스와 달리, 함수가 맵을 반환하지 않고도 채울 수 있습니다.
nil 맵 패닉
맵의 제로 값은 nil입니다. nil 맵은 빈 맵처럼 읽히지만, 쓰면 패닉이 납니다.
출력:
0 0
recovered: assignment to entry in nil map
실제로 문제가 되는 것은 구조체의 경우입니다. 맵 필드는 생성자에서 초기화하거나(func NewCache() *Cache { return &Cache{data: map[string]string{}} }) 첫 쓰기 전에 지연 초기화하세요.
동시 접근
맵은 동시에 써도 안전하지 않습니다. 한 고루틴이 쓰는 동안 다른 고루틴이 읽거나 쓰면, 런타임이 fatal error: concurrent map writes(또는 concurrent map read and map write)로 프로그램을 멈출 수 있습니다. 패닉이 아니라 치명적 오류이므로 recover로 잡을 수 없습니다.
뮤텍스로 맵을 보호하세요:
이 코드는 항상 50 50을 출력합니다. 읽기가 쓰기보다 훨씬 많다면 sync.RWMutex를 쓰세요. sync.Map은 두 가지 좁은 경우(키가 한 번 쓰이고 여러 번 읽힘, 또는 고루틴들이 서로 다른 키만 다룸)를 위한 것이며, 그 밖에는 뮤텍스와 일반 맵이 더 단순하고 대개 더 빠릅니다. 자세한 내용은 뮤텍스에 있습니다.
빠른 참조
| 연산 | 코드 |
|---|---|
| 생성 | m := map[string]int{} 또는 make(map[string]int) |
| 삽입 또는 갱신 | m[k] = v |
| 읽기(없으면 제로 값) | v := m[k] |
| 존재 확인 | v, ok := m[k] |
| 삭제 | delete(m, k) |
| 전부 제거 | clear(m) (Go 1.21) |
| 크기 | len(m) |
| 정렬된 키 | slices.Sorted(maps.Keys(m)) (Go 1.23) |
| 복사 | maps.Clone(m) |
| 비교 | maps.Equal(a, b) |
값이 struct{}인 맵은 Go의 집합 타입이기도 합니다. 집합을 참고하세요.
흔한 실수
- nil 맵에 씀. 구조체의 맵 필드를 포함해 항상
make하세요. - 순회 순서에 의존함. 키를 정렬하세요.
m[k] != 0을 존재 확인으로 씀. comma-ok를 쓰세요.m[k].Field로 구조체 필드를 수정함. 꺼내서 바꾼 뒤 다시 쓰거나 포인터를 저장하세요.- 잠금 없이 고루틴 사이에서 맵을 공유함. 그 충돌은 복구할 수 없습니다.
자주 묻는 질문
Go 맵에 키가 있는지 어떻게 확인하나요?
조회의 두 값 형태를 씁니다: v, ok := m[key]. 키가 있으면 ok가 true, 없으면 false이고, 없을 때 v는 제로 값입니다. m[key]만 읽어서는 없는 키와 제로 값으로 저장된 키를 구분할 수 없습니다.
Go 맵의 순회 순서가 무작위인 이유는 무엇인가요?
언어가 순서를 정의하지 않고, 프로그램이 특정 순서에 의존하지 못하도록 런타임이 일부러 range마다 무작위 위치에서 시작하기 때문입니다. 키 순서로 순회하려면 키를 모아 정렬하세요: for _, k := range slices.Sorted(maps.Keys(m))(Go 1.23).
Go에서 맵의 모든 키를 얻으려면 어떻게 하나요?
Go 1.23부터 maps.Keys(m)는 이터레이터를 반환합니다. slices.Collect(maps.Keys(m))로 슬라이스로, slices.Sorted(maps.Keys(m))로 정렬된 슬라이스로 바꾸세요. 1.23 이전에는 for k := range m으로 반복하면서 각 키를 슬라이스에 append했습니다.
맵에 쓸 때 "assignment to entry in nil map" 패닉이 나는 이유는 무엇인가요?
맵 변수를 선언만 하고 만들지 않았기 때문입니다. var m map[string]int는 nil입니다. nil 맵을 읽으면 제로 값이 나오지만 쓰면 패닉이 납니다. 먼저 m = make(map[string]int)나 리터럴 m := map[string]int{}로 만드세요. 구조체 안의 맵 필드도 같은 초기화가 필요합니다.
Go 맵은 동시에 써도 안전한가요?
아니요. 동시 쓰기, 또는 읽기와 동시에 일어나는 쓰기는 fatal error: concurrent map writes로 프로그램을 죽일 수 있으며, 이는 recover로 잡을 수 없습니다. 맵을 sync.Mutex나 sync.RWMutex로 보호하거나, 설계된 특정 용도라면 sync.Map을 쓰세요.