[Go] map
Updated:
개요
맵(map)은 키-값 쌍을 저장하는 해시 테이블 기반 자료구조입니다.
주요 특징:
- 키-값 쌍: 고유한 키로 값에 접근
- 참조 타입: 함수 인자로 전달 시 참조 전달
- 제로값은 nil: nil 맵에 쓰기 시도는 패닉 발생
- 순서 없음: 순회 순서가 무작위 (의도적 설계)
- 동적 크기: 자동으로 확장
make()또는 맵 리터럴로 생성- Comparable 키: 키 타입은
==비교 가능해야 함
맵의 내부 구조
// 맵의 개념적 구조
// map[KeyType]ValueType
// 내부적으로 해시 테이블로 구현
// 키의 해시값으로 버킷 위치 결정
해시 테이블 구조:
┌─────────────────────────────────┐
│ Key Hash → Bucket Index │
├─────────────────────────────────┤
│ Bucket 0: [key1:val1, key2:val2]│
│ Bucket 1: [key3:val3] │
│ Bucket 2: [] │
│ Bucket 3: [key4:val4, key5:val5]│
└─────────────────────────────────┘
맵 생성 방법
1. nil 맵
package main
import "fmt"
func nilMap() {
var m map[string]int
fmt.Println(m) // map[]
fmt.Println(m == nil) // true
fmt.Println(len(m)) // 0
// 읽기는 가능 (제로값 반환)
value := m["key"]
fmt.Println(value) // 0
// ❌ nil 맵에 쓰기는 패닉!
// m["key"] = 1 // panic: assignment to entry in nil map
}
2. make로 생성
func makeMap() {
// 기본 생성
m1 := make(map[string]int)
fmt.Println(m1 == nil) // false
// 초기 용량 힌트 (성능 최적화)
m2 := make(map[string]int, 100)
// 쓰기 가능
m1["apple"] = 1
m2["banana"] = 2
fmt.Println(m1) // map[apple:1]
fmt.Println(m2) // map[banana:2]
}
3. 맵 리터럴
func mapLiteral() {
// 빈 맵
m1 := map[string]int{}
fmt.Println(m1 == nil) // false
// 초기값과 함께 생성
m2 := map[string]int{
"apple": 1,
"banana": 2,
"cherry": 3,
}
// 복잡한 타입
m3 := map[int][]string{
1: {"a", "b"},
2: {"c", "d"},
}
// 구조체를 값으로
type Person struct {
Name string
Age int
}
m4 := map[string]Person{
"alice": {"Alice", 25},
"bob": {"Bob", 30},
}
fmt.Println(m2, m3, m4)
}
맵 접근 및 수정
1. 기본 접근
func mapAccess() {
m := map[string]int{
"apple": 1,
"banana": 2,
}
// 값 읽기
fmt.Println(m["apple"]) // 1
// 존재하지 않는 키 (제로값 반환)
fmt.Println(m["cherry"]) // 0
// 값 쓰기
m["cherry"] = 3
fmt.Println(m["cherry"]) // 3
// 값 수정
m["apple"] = 10
fmt.Println(m["apple"]) // 10
}
2. Comma Ok Idiom (존재 여부 확인)
func commaOk() {
m := map[string]int{
"apple": 1,
"banana": 0, // 제로값 저장
}
// 단순 접근 (존재 여부 불확실)
value := m["banana"]
fmt.Println(value) // 0 (존재하지만 제로값)
value = m["cherry"]
fmt.Println(value) // 0 (존재하지 않음)
// Comma Ok Idiom (존재 여부 확인)
value, ok := m["banana"]
fmt.Printf("banana: value=%d, exists=%v\n", value, ok)
// banana: value=0, exists=true
value, ok = m["cherry"]
fmt.Printf("cherry: value=%d, exists=%v\n", value, ok)
// cherry: value=0, exists=false
// 일반적인 패턴
if value, ok := m["apple"]; ok {
fmt.Println("Found:", value)
} else {
fmt.Println("Not found")
}
}
3. 맵 삭제
func deleteFromMap() {
m := map[string]int{
"apple": 1,
"banana": 2,
"cherry": 3,
}
fmt.Println("Before:", m) // map[apple:1 banana:2 cherry:3]
// 키 삭제
delete(m, "banana")
fmt.Println("After:", m) // map[apple:1 cherry:3]
// 존재하지 않는 키 삭제 (안전, 아무 일도 안 일어남)
delete(m, "orange")
// 모든 요소 삭제
for key := range m {
delete(m, key)
}
fmt.Println("Cleared:", m) // map[]
// 또는 새 맵 할당
m = make(map[string]int)
}
맵 순회
func iterateMap() {
m := map[string]int{
"apple": 1,
"banana": 2,
"cherry": 3,
}
// 키와 값 모두 순회
for key, value := range m {
fmt.Printf("%s: %d\n", key, value)
}
// 키만 순회
for key := range m {
fmt.Println(key)
}
// 값만 순회 (비권장, 키 없이는 의미 없음)
for _, value := range m {
fmt.Println(value)
}
// ⚠️ 순회 순서는 무작위!
// 같은 맵을 여러 번 순회해도 순서가 다를 수 있음
}
정렬된 순서로 순회
import "sort"
func sortedIterate() {
m := map[string]int{
"charlie": 3,
"alice": 1,
"bob": 2,
}
// 키를 슬라이스로 추출
keys := make([]string, 0, len(m))
for key := range m {
keys = append(keys, key)
}
// 키 정렬
sort.Strings(keys)
// 정렬된 순서로 접근
for _, key := range keys {
fmt.Printf("%s: %d\n", key, m[key])
}
// alice: 1
// bob: 2
// charlie: 3
}
맵은 참조 타입
func mapReference() {
m1 := map[string]int{"apple": 1}
// 맵 할당은 참조 복사
m2 := m1
m2["banana"] = 2
fmt.Println(m1) // map[apple:1 banana:2] (변경됨!)
fmt.Println(m2) // map[apple:1 banana:2]
// 함수 인자로 전달
modifyMap(m1)
fmt.Println(m1) // map[apple:1 banana:2 cherry:3]
}
func modifyMap(m map[string]int) {
m["cherry"] = 3 // 원본 맵 수정됨
}
맵 복사
func copyMap() {
original := map[string]int{
"apple": 1,
"banana": 2,
}
// 얕은 복사 (수동)
copied := make(map[string]int)
for key, value := range original {
copied[key] = value
}
copied["cherry"] = 3
fmt.Println("Original:", original) // map[apple:1 banana:2]
fmt.Println("Copied:", copied) // map[apple:1 banana:2 cherry:3]
}
// 제네릭 복사 함수 (Go 1.18+)
func CopyMap[K comparable, V any](m map[K]V) map[K]V {
result := make(map[K]V, len(m))
for k, v := range m {
result[k] = v
}
return result
}
키 타입 제약
func keyTypes() {
// ✅ Comparable 타입은 키로 사용 가능
m1 := map[int]string{}
m2 := map[string]int{}
m3 := map[bool]int{}
m4 := map[float64]string{} // 부동소수점은 가능하지만 권장 안 함
type Point struct {
X, Y int
}
m5 := map[Point]string{} // 구조체 (모든 필드가 comparable)
// ❌ 슬라이스는 키로 사용 불가
// m6 := map[[]int]string{} // 컴파일 에러
// ❌ 맵은 키로 사용 불가
// m7 := map[map[string]int]string{} // 컴파일 에러
// ❌ 함수는 키로 사용 불가
// m8 := map[func()]string{} // 컴파일 에러
// ✅ 포인터는 키로 사용 가능
m9 := map[*Point]string{}
// ✅ 인터페이스는 키로 사용 가능 (런타임 값이 comparable이어야 함)
m10 := map[interface{}]string{}
_, _, _, _, _, _, _, _ = m1, m2, m3, m4, m5, m9, m10
}
동시성과 sync.Map
import (
"sync"
)
// ❌ 일반 맵은 동시성 안전하지 않음
func unsafeMap() {
m := make(map[int]int)
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
m[n] = n * 2 // 경쟁 상태 발생 가능
}(i)
}
wg.Wait()
// 패닉 또는 데이터 손실 가능
}
// ✅ Mutex로 보호
func safeMapWithMutex() {
m := make(map[int]int)
var mu sync.Mutex
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
mu.Lock()
m[n] = n * 2
mu.Unlock()
}(i)
}
wg.Wait()
}
// ✅ sync.Map 사용 (특정 상황에서 유용)
func syncMap() {
var m sync.Map
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
m.Store(n, n*2)
}(i)
}
wg.Wait()
// 값 읽기
if value, ok := m.Load(42); ok {
fmt.Println("Found:", value)
}
// 순회
m.Range(func(key, value interface{}) bool {
fmt.Printf("%v: %v\n", key, value)
return true // false 반환 시 순회 중단
})
}
실전 활용 패턴
1. 카운터 (빈도수 계산)
func wordCount(text string) map[string]int {
words := strings.Fields(text)
counts := make(map[string]int)
for _, word := range words {
counts[word]++
}
return counts
}
func main() {
text := "go go go python java go python"
counts := wordCount(text)
fmt.Println(counts) // map[go:4 java:1 python:2]
}
2. 집합 (Set) 구현
type Set map[string]bool
func NewSet() Set {
return make(Set)
}
func (s Set) Add(item string) {
s[item] = true
}
func (s Set) Remove(item string) {
delete(s, item)
}
func (s Set) Contains(item string) bool {
return s[item]
}
func (s Set) Size() int {
return len(s)
}
func (s Set) Items() []string {
items := make([]string, 0, len(s))
for item := range s {
items = append(items, item)
}
return items
}
func main() {
s := NewSet()
s.Add("apple")
s.Add("banana")
s.Add("apple") // 중복 추가 (무시됨)
fmt.Println(s.Contains("apple")) // true
fmt.Println(s.Size()) // 2
fmt.Println(s.Items()) // [apple banana] (순서 무작위)
}
3. 그룹화
type Student struct {
Name string
Grade int
}
func groupByGrade(students []Student) map[int][]Student {
groups := make(map[int][]Student)
for _, student := range students {
groups[student.Grade] = append(groups[student.Grade], student)
}
return groups
}
func main() {
students := []Student{
{"Alice", 1},
{"Bob", 2},
{"Charlie", 1},
{"David", 2},
{"Eve", 3},
}
groups := groupByGrade(students)
for grade, students := range groups {
fmt.Printf("Grade %d: %v\n", grade, students)
}
}
4. 캐시 구현
type Cache struct {
data map[string]interface{}
mu sync.RWMutex
}
func NewCache() *Cache {
return &Cache{
data: make(map[string]interface{}),
}
}
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
value, ok := c.data[key]
return value, ok
}
func (c *Cache) Set(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
func (c *Cache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.data, key)
}
func (c *Cache) Clear() {
c.mu.Lock()
defer c.mu.Unlock()
c.data = make(map[string]interface{})
}
5. 인덱스 구현
type Book struct {
ISBN string
Title string
Author string
}
type Library struct {
booksByISBN map[string]*Book
booksByAuthor map[string][]*Book
}
func NewLibrary() *Library {
return &Library{
booksByISBN: make(map[string]*Book),
booksByAuthor: make(map[string][]*Book),
}
}
func (l *Library) AddBook(book *Book) {
l.booksByISBN[book.ISBN] = book
l.booksByAuthor[book.Author] = append(l.booksByAuthor[book.Author], book)
}
func (l *Library) FindByISBN(isbn string) *Book {
return l.booksByISBN[isbn]
}
func (l *Library) FindByAuthor(author string) []*Book {
return l.booksByAuthor[author]
}
6. 메모이제이션
func fibonacci() func(int) int {
cache := make(map[int]int)
var fib func(int) int
fib = func(n int) int {
if n <= 1 {
return n
}
if val, ok := cache[n]; ok {
return val
}
result := fib(n-1) + fib(n-2)
cache[n] = result
return result
}
return fib
}
func main() {
fib := fibonacci()
fmt.Println(fib(10)) // 55
fmt.Println(fib(40)) // 102334155 (캐시로 빠름)
}
7. 그래프 표현
type Graph map[string][]string
func NewGraph() Graph {
return make(Graph)
}
func (g Graph) AddEdge(from, to string) {
g[from] = append(g[from], to)
}
func (g Graph) Neighbors(node string) []string {
return g[node]
}
func main() {
graph := NewGraph()
graph.AddEdge("A", "B")
graph.AddEdge("A", "C")
graph.AddEdge("B", "D")
graph.AddEdge("C", "D")
fmt.Println("Neighbors of A:", graph.Neighbors("A"))
// [B C]
}
8. 설정 관리
type Config map[string]interface{}
func (c Config) GetString(key string, defaultValue string) string {
if val, ok := c[key]; ok {
if str, ok := val.(string); ok {
return str
}
}
return defaultValue
}
func (c Config) GetInt(key string, defaultValue int) int {
if val, ok := c[key]; ok {
if num, ok := val.(int); ok {
return num
}
}
return defaultValue
}
func (c Config) GetBool(key string, defaultValue bool) bool {
if val, ok := c[key]; ok {
if b, ok := val.(bool); ok {
return b
}
}
return defaultValue
}
func main() {
config := Config{
"host": "localhost",
"port": 8080,
"debug": true,
"timeout": 30,
}
host := config.GetString("host", "0.0.0.0")
port := config.GetInt("port", 3000)
debug := config.GetBool("debug", false)
fmt.Printf("Server: %s:%d (debug=%v)\n", host, port, debug)
}
제네릭 맵 함수 (Go 1.18+)
// 맵 키 추출
func Keys[K comparable, V any](m map[K]V) []K {
keys := make([]K, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
// 맵 값 추출
func Values[K comparable, V any](m map[K]V) []V {
values := make([]V, 0, len(m))
for _, v := range m {
values = append(values, v)
}
return values
}
// 맵 필터
func Filter[K comparable, V any](m map[K]V, fn func(K, V) bool) map[K]V {
result := make(map[K]V)
for k, v := range m {
if fn(k, v) {
result[k] = v
}
}
return result
}
// 맵 변환
func MapValues[K comparable, V, U any](m map[K]V, fn func(V) U) map[K]U {
result := make(map[K]U, len(m))
for k, v := range m {
result[k] = fn(v)
}
return result
}
func main() {
m := map[string]int{
"apple": 1,
"banana": 2,
"cherry": 3,
}
// 키 추출
keys := Keys(m)
fmt.Println(keys) // [apple banana cherry] (순서 무작위)
// 값 추출
values := Values(m)
fmt.Println(values) // [1 2 3] (순서 무작위)
// 필터: 값이 2 이상인 항목만
filtered := Filter(m, func(k string, v int) bool {
return v >= 2
})
fmt.Println(filtered) // map[banana:2 cherry:3]
// 변환: 모든 값을 2배로
doubled := MapValues(m, func(v int) int {
return v * 2
})
fmt.Println(doubled) // map[apple:2 banana:4 cherry:6]
}
성능 고려사항
1. 초기 용량 설정
import "testing"
func BenchmarkMapWithoutCapacity(b *testing.B) {
for i := 0; i < b.N; i++ {
m := make(map[int]int)
for j := 0; j < 1000; j++ {
m[j] = j
}
}
}
func BenchmarkMapWithCapacity(b *testing.B) {
for i := 0; i < b.N; i++ {
m := make(map[int]int, 1000)
for j := 0; j < 1000; j++ {
m[j] = j
}
}
}
// 결과: 용량 힌트를 주면 약 20-30% 빠름
2. 맵 vs 슬라이스 (작은 데이터)
// 요소가 적으면 슬라이스가 더 빠를 수 있음
func findInMap(m map[string]int, key string) (int, bool) {
val, ok := m[key]
return val, ok
}
type KeyValue struct {
Key string
Value int
}
func findInSlice(slice []KeyValue, key string) (int, bool) {
for _, kv := range slice {
if kv.Key == key {
return kv.Value, true
}
}
return 0, false
}
// 요소 < 10개: 슬라이스가 빠름
// 요소 > 100개: 맵이 빠름
3. 문자열 키 최적화
// ❌ 긴 문자열 키는 비효율적
m1 := make(map[string]int)
m1["very_long_key_name_that_takes_memory"] = 1
// ✅ 짧은 문자열 또는 정수 키 권장
m2 := make(map[int]int)
m2[1] = 1
// ✅ 또는 문자열 인턴 패턴 사용
type StringInterner struct {
strings map[string]string
}
func (si *StringInterner) Intern(s string) string {
if interned, ok := si.strings[s]; ok {
return interned
}
si.strings[s] = s
return s
}
일반적인 실수
1. nil 맵에 쓰기
func mistake1() {
var m map[string]int // nil 맵
// ❌ 패닉 발생!
// m["key"] = 1 // panic: assignment to entry in nil map
// ✅ 초기화 후 사용
m = make(map[string]int)
m["key"] = 1
}
2. 맵 순회 순서 의존
func mistake2() {
m := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
// ❌ 순서에 의존하는 코드
var keys []string
for k := range m {
keys = append(keys, k)
}
// keys 순서는 실행마다 다름!
// ✅ 명시적으로 정렬
sort.Strings(keys)
}
3. 맵 비교
func mistake3() {
m1 := map[string]int{"a": 1}
m2 := map[string]int{"a": 1}
// ❌ 컴파일 에러: 맵은 == 비교 불가
// fmt.Println(m1 == m2)
// ✅ nil 비교만 가능
var m3 map[string]int
fmt.Println(m3 == nil) // true
// ✅ 수동 비교
equal := len(m1) == len(m2)
if equal {
for k, v := range m1 {
if m2[k] != v {
equal = false
break
}
}
}
// ✅ reflect 사용
fmt.Println(reflect.DeepEqual(m1, m2)) // true
}
4. 맵 순회 중 수정
func mistake4() {
m := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
// ⚠️ 순회 중 삭제는 안전하지만 예측 불가능
for k, v := range m {
if v%2 == 0 {
delete(m, k) // 안전하지만 순회 결과 예측 어려움
}
}
// ✅ 삭제할 키를 먼저 수집
toDelete := []string{}
for k, v := range m {
if v%2 == 0 {
toDelete = append(toDelete, k)
}
}
for _, k := range toDelete {
delete(m, k)
}
}
5. 구조체 필드 수정 시도
type Person struct {
Name string
Age int
}
func mistake5() {
m := map[string]Person{
"alice": {"Alice", 25},
}
// ❌ 컴파일 에러: 맵 요소는 addressable하지 않음
// m["alice"].Age = 26
// ✅ 전체 값 재할당
p := m["alice"]
p.Age = 26
m["alice"] = p
// ✅ 또는 포인터 사용
m2 := map[string]*Person{
"alice": {"Alice", 25},
}
m2["alice"].Age = 26 // OK
}
6. 동시성 문제
func mistake6() {
m := make(map[int]int)
// ❌ 데이터 레이스
go func() {
m[1] = 1
}()
go func() {
m[2] = 2
}()
// ✅ Mutex 사용
var mu sync.Mutex
go func() {
mu.Lock()
m[1] = 1
mu.Unlock()
}()
}
맵 관련 팁
1. 존재하지 않는 키의 기본값
func getWithDefault(m map[string]int, key string, defaultValue int) int {
if value, ok := m[key]; ok {
return value
}
return defaultValue
}
// 또는 제로값 활용
func increment(m map[string]int, key string) {
m[key]++ // 존재하지 않으면 0에서 시작
}
2. 맵을 슬라이스로 변환
type Entry struct {
Key string
Value int
}
func mapToSlice(m map[string]int) []Entry {
entries := make([]Entry, 0, len(m))
for k, v := range m {
entries = append(entries, Entry{k, v})
}
return entries
}
3. 조건부 초기화
func ensureKey(m map[string][]int, key string) {
if _, ok := m[key]; !ok {
m[key] = []int{}
}
}
// 사용
m := make(map[string][]int)
ensureKey(m, "items")
m["items"] = append(m["items"], 1, 2, 3)
정리
- 맵은 키-값 쌍을 저장하는 해시 테이블
- 참조 타입: 함수 전달 시 원본 수정됨
- nil 맵: 읽기는 가능하지만 쓰기는 패닉
make()또는 리터럴로 초기화 필수- 순서 없음: 순회 순서는 무작위 (의도적)
- Comma Ok Idiom:
value, ok := m[key]로 존재 여부 확인 delete(m, key)로 삭제- 키는 comparable 타입만 가능
- 동시성 안전 아님: Mutex 또는 sync.Map 사용
- 초기 용량 힌트로 성능 개선 가능
- 맵은
==비교 불가 (nil 비교만 가능) - 구조체 필드 직접 수정 불가 (값 재할당 또는 포인터 사용)
- 작은 데이터(<10개)는 슬라이스가 더 빠를 수 있음