본문으로 건너뛰기

맵(Map)

맵이란?

맵은 키(key)와 값(value)의 쌍을 저장하는 해시 테이블 기반 자료구조입니다. 키로 값을 O(1)에 조회할 수 있어 빠른 검색이 필요할 때 유용합니다.

Go의 맵은 참조 타입으로, 함수에 전달해도 내부 해시 테이블을 공유합니다. 선언만 한 nil 맵에 쓰기를 시도하면 패닉이 발생합니다.


맵 선언과 초기화

package main

import "fmt"

func main() {
// 1. nil 맵 (읽기는 가능, 쓰기는 패닉)
var m1 map[string]int
fmt.Println(m1 == nil) // true
fmt.Println(m1["key"]) // 0 — 읽기는 가능 (제로값 반환)
// m1["key"] = 1 // 런타임 패닉!

// 2. make로 초기화 (권장)
m2 := make(map[string]int)
m2["alice"] = 90
m2["bob"] = 85
fmt.Println(m2) // map[alice:90 bob:85]

// 3. 맵 리터럴
m3 := map[string]int{
"alice": 90,
"bob": 85,
"carol": 78,
}
fmt.Println(m3)

// 4. 초기 용량 힌트 (성능 최적화)
m4 := make(map[string]int, 100) // 100개 이상 넣을 예정
_ = m4
}

CRUD 연산

package main

import "fmt"

func main() {
scores := map[string]int{
"alice": 90,
"bob": 85,
}

// Create / Update — 동일 문법
scores["carol"] = 78 // 새 키 추가
scores["alice"] = 95 // 기존 키 업데이트

// Read
fmt.Println(scores["alice"]) // 95
fmt.Println(scores["none"]) // 0 (존재하지 않는 키 → 제로값)

// 키 존재 여부 확인 (comma-ok 패턴)
val, ok := scores["bob"]
if ok {
fmt.Printf("bob의 점수: %d\n", val)
}

_, exists := scores["dave"]
fmt.Println("dave 존재:", exists) // false

// Delete
delete(scores, "bob")
fmt.Println(scores) // map[alice:95 carol:78]

// 존재하지 않는 키를 delete해도 패닉 없음
delete(scores, "nobody")
}

맵 순회

맵의 순회 순서는 매 실행마다 무작위 입니다. 순서가 중요하다면 키를 슬라이스에 담아 정렬 후 순회하세요.

package main

import (
"fmt"
"sort"
)

func main() {
m := map[string]int{
"banana": 3,
"apple": 5,
"cherry": 2,
}

// 기본 순회 — 순서 불보장
for k, v := range m {
fmt.Printf("%s: %d\n", k, v)
}

fmt.Println("---정렬 순회---")

// 정렬된 순회
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%s: %d\n", k, m[k])
}
// apple: 5
// banana: 3
// cherry: 2
}

중첩 맵과 복잡한 값 타입

package main

import "fmt"

func main() {
// 맵의 값이 슬라이스인 경우 (그룹핑)
groups := map[string][]string{
"backend": {"alice", "bob"},
"frontend": {"carol", "dave"},
}
groups["backend"] = append(groups["backend"], "eve")
fmt.Println(groups)
// map[backend:[alice bob eve] frontend:[carol dave]]

// 중첩 맵 (주의: 내부 맵도 반드시 초기화)
nested := map[string]map[string]int{}
nested["team1"] = map[string]int{"alice": 90, "bob": 85}
nested["team2"] = map[string]int{"carol": 78}
fmt.Println(nested["team1"]["alice"]) // 90

// 잘못된 패턴 — 내부 맵 초기화 없이 쓰면 패닉
// var bad map[string]map[string]int
// bad["k1"]["k2"] = 1 // 패닉!
}

맵과 구조체 조합

실무에서는 맵의 값으로 구조체를 많이 사용합니다.

package main

import "fmt"

type Student struct {
Name string
Score int
Grade string
}

func toGrade(score int) string {
switch {
case score >= 90:
return "A"
case score >= 80:
return "B"
case score >= 70:
return "C"
default:
return "F"
}
}

func main() {
students := map[string]Student{
"s001": {Name: "Alice", Score: 92},
"s002": {Name: "Bob", Score: 78},
"s003": {Name: "Carol", Score: 85},
}

// 구조체 필드 업데이트: 직접 필드 수정 불가 — 전체 교체
s := students["s001"]
s.Grade = toGrade(s.Score)
students["s001"] = s

for id, st := range students {
fmt.Printf("[%s] %s: %d점 (%s)\n", id, st.Name, st.Score, st.Grade)
}
}

주의: 맵에서 구조체 값의 필드를 students["s001"].Grade = "A" 처럼 직접 수정할 수 없습니다. 값 타입이기 때문에 전체를 꺼내서 수정 후 다시 넣어야 합니다. 포인터(map[string]*Student)를 사용하면 직접 수정이 가능합니다.


동시성 주의사항

Go의 기본 맵은 동시성 안전(thread-safe)하지 않습니다. 여러 고루틴이 동시에 맵을 읽고 쓰면 런타임 패닉이 발생합니다.

package main

import (
"fmt"
"sync"
)

// sync.RWMutex로 동시성 안전 맵 구현
type SafeMap struct {
mu sync.RWMutex
m map[string]int
}

func NewSafeMap() *SafeMap {
return &SafeMap{m: make(map[string]int)}
}

func (sm *SafeMap) Set(key string, val int) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.m[key] = val
}

func (sm *SafeMap) Get(key string) (int, bool) {
sm.mu.RLock()
defer sm.mu.RUnlock()
v, ok := sm.m[key]
return v, ok
}

func main() {
sm := NewSafeMap()

var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
key := fmt.Sprintf("key%d", n%10)
sm.Set(key, n)
}(i)
}
wg.Wait()

if v, ok := sm.Get("key0"); ok {
fmt.Println("key0:", v)
}

// 표준 라이브러리 sync.Map — 고루틴 안전, 읽기 많은 경우에 최적화
var syncMap sync.Map
syncMap.Store("hello", 42)
if val, ok := syncMap.Load("hello"); ok {
fmt.Println(val.(int)) // 42
}
syncMap.Range(func(k, v any) bool {
fmt.Printf("%v: %v\n", k, v)
return true // false를 반환하면 순회 중단
})
}

실전 예제 — 단어 빈도 카운터

package main

import (
"fmt"
"sort"
"strings"
)

type WordCount struct {
Word string
Count int
}

func topN(text string, n int) []WordCount {
freq := make(map[string]int)
for _, word := range strings.Fields(strings.ToLower(text)) {
// 구두점 제거
word = strings.Trim(word, ".,!?;:")
if word != "" {
freq[word]++
}
}

counts := make([]WordCount, 0, len(freq))
for word, count := range freq {
counts = append(counts, WordCount{word, count})
}

sort.Slice(counts, func(i, j int) bool {
if counts[i].Count != counts[j].Count {
return counts[i].Count > counts[j].Count
}
return counts[i].Word < counts[j].Word
})

if n > len(counts) {
n = len(counts)
}
return counts[:n]
}

func main() {
text := "go is great go is fast go is simple and go is powerful"
for _, wc := range topN(text, 5) {
fmt.Printf("%-10s %d\n", wc.Word, wc.Count)
}
// go 4
// is 4
// and 1
// fast 1
// great 1
}