Menu

Golang 맵: 생성, 키 확인, 순회, 정렬, 삭제

Go 맵은 키-값 쌍을 저장하고 빠르게 조회합니다. 맵을 만들고, comma-ok로 키가 있는지 확인하고, 삭제하고, (무작위 순서로) 순회하고, 키를 정렬하고, 구조체를 저장하는 방법과 nil 맵 및 동시 쓰기 패닉을 피하는 방법을 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

맵 만들고 쓰기

맵 타입은 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 { ... } 형태는 vok의 스코프를 if로 한정합니다. Go 코드에서 가장 흔한 줄 중 하나입니다.

항목 삭제하기

delete(m, key)는 항목을 제거합니다. 없는 키를 삭제해도 아무 일도 일어나지 않고, nil 맵에서 삭제해도 마찬가지입니다. 맵 전체를 비우려면 Go 1.21에 추가된 clear(m)을 쓰세요. 할당된 맵을 유지하므로 재사용할 수 있습니다.

같은 맵을 range로 순회하는 동안 항목을 삭제해도 허용되고 안전합니다. 반복문이 도달하기 전에 삭제된 항목은 나오지 않습니다.

순회: 순서는 무작위다

for k, v := range m은 모든 항목을 정해지지 않은 순서로 한 번씩 방문합니다. 런타임이 일부러 시작 지점을 무작위로 정하므로, 같은 프로그램에서 같은 맵을 두 번 순회해도 순서가 다른 경우가 많습니다. 몇 번 실행해 보세요:

출력이 맵 순서에 의존하는 코드는 다음 실행을 기다리는 버그입니다. 출력된 맵 순회 결과를 비교하는 테스트가 전형적인 예입니다.

정렬된 키

키 순서로 맵을 방문하려면 키를 얻어 정렬한 뒤 맵을 인덱싱하세요. Go 1.23은 mapsslices 패키지의 이터레이터로 이를 한 줄로 만들었습니다:

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]. 키가 있으면 oktrue, 없으면 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.Mutexsync.RWMutex로 보호하거나, 설계된 특정 용도라면 sync.Map을 쓰세요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기