Updated:

13 minute read

개요

Go 1.21부터 추가된 maps 패키지는 제네릭 기반의 맵 조작 함수들을 제공합니다.

주요 특징:

  • 제네릭 기반: 모든 타입의 맵 지원
  • 표준 라이브러리: import “maps”
  • 순회: All, Keys, Values로 다양한 방식 순회
  • 복사: Clone (얕은 복사), Copy (병합)
  • 비교: Equal, EqualFunc로 맵 비교
  • 수정: Insert, DeleteFunc로 조작
  • 변환: Collect로 Iterator를 맵으로 변환
  • 안전성: nil 맵 처리 포함

기본 순회

1. All - 키와 값 모두

import "maps"

func main() {
    m := map[string]int{
        "apple":  1,
        "banana": 2,
        "cherry": 3,
    }
    
    // 키와 값 모두 순회
    for k, v := range maps.All(m) {
        fmt.Printf("%s: %d\n", k, v)
    }
    // apple: 1
    // banana: 2
    // cherry: 3
}

2. Keys - 키만

func main() {
    m := map[string]int{
        "apple":  1,
        "banana": 2,
        "cherry": 3,
    }
    
    // 키만 순회
    for k := range maps.Keys(m) {
        fmt.Println(k)
    }
    // apple
    // banana
    // cherry
    
    // 키를 슬라이스로 수집
    keys := make([]string, 0)
    for k := range maps.Keys(m) {
        keys = append(keys, k)
    }
    fmt.Println(keys)  // [apple banana cherry]
}

3. Values - 값만

func main() {
    m := map[string]int{
        "apple":  1,
        "banana": 2,
        "cherry": 3,
    }
    
    // 값만 순회
    for v := range maps.Values(m) {
        fmt.Println(v)
    }
    // 1
    // 2
    // 3
    
    // 합계 계산
    sum := 0
    for v := range maps.Values(m) {
        sum += v
    }
    fmt.Println("Sum:", sum)  // Sum: 6
}

4. Collect - Iterator를 맵으로

func main() {
    // Iterator 생성
    seq := maps.All(map[string]int{
        "a": 1,
        "b": 2,
        "c": 3,
    })
    
    // 맵으로 변환
    result := maps.Collect(seq)
    fmt.Println(result)  // map[a:1 b:2 c:3]
}

복사 함수

1. Clone - 얕은 복사

func main() {
    original := map[string]int{
        "a": 1,
        "b": 2,
        "c": 3,
    }
    
    // 얕은 복사
    cloned := maps.Clone(original)
    fmt.Println(cloned)  // map[a:1 b:2 c:3]
    
    // 원본 수정해도 복사본은 영향 없음
    original["a"] = 100
    fmt.Println(original)  // map[a:100 b:2 c:3]
    fmt.Println(cloned)    // map[a:1 b:2 c:3]
    
    // nil 맵도 안전
    var nilMap map[string]int
    clonedNil := maps.Clone(nilMap)
    fmt.Println(clonedNil == nil)  // true
}

2. Clone with 포인터 값

type Person struct {
    Name string
    Age  int
}

func main() {
    original := map[string]*Person{
        "alice": {Name: "Alice", Age: 30},
        "bob":   {Name: "Bob", Age: 25},
    }
    
    // 포인터는 공유됨 (얕은 복사)
    cloned := maps.Clone(original)
    
    // 원본의 포인터가 가리키는 값 수정
    original["alice"].Age = 31
    
    fmt.Println(original["alice"].Age)  // 31
    fmt.Println(cloned["alice"].Age)    // 31 (같은 객체)
}

3. Copy - 맵 병합

func main() {
    dst := map[string]int{
        "a": 1,
        "b": 2,
    }
    
    src := map[string]int{
        "b": 20,  // 덮어씀
        "c": 3,   // 새로 추가
    }
    
    // src를 dst로 복사 (병합)
    maps.Copy(dst, src)
    fmt.Println(dst)  // map[a:1 b:20 c:3]
}

4. Copy 활용 - 여러 맵 병합

func main() {
    m1 := map[string]int{"a": 1}
    m2 := map[string]int{"b": 2}
    m3 := map[string]int{"c": 3}
    
    // 모두 병합
    result := make(map[string]int)
    maps.Copy(result, m1)
    maps.Copy(result, m2)
    maps.Copy(result, m3)
    
    fmt.Println(result)  // map[a:1 b:2 c:3]
}

비교 함수

1. Equal - 동등 비교

func main() {
    m1 := map[string]int{"a": 1, "b": 2}
    m2 := map[string]int{"a": 1, "b": 2}
    m3 := map[string]int{"a": 1, "b": 3}
    m4 := map[string]int{"a": 1}
    
    fmt.Println(maps.Equal(m1, m2))  // true
    fmt.Println(maps.Equal(m1, m3))  // false (값 다름)
    fmt.Println(maps.Equal(m1, m4))  // false (길이 다름)
    
    // nil 맵 비교
    var n1, n2 map[string]int
    fmt.Println(maps.Equal(n1, n2))  // true (둘 다 nil)
    
    // nil과 빈 맵
    empty := make(map[string]int)
    fmt.Println(maps.Equal(n1, empty))  // true (둘 다 빈 맵)
}

2. EqualFunc - 커스텀 비교

import "strings"

func main() {
    m1 := map[string]string{
        "a": "hello",
        "b": "world",
    }
    
    m2 := map[string]string{
        "a": "HELLO",
        "b": "WORLD",
    }
    
    // 대소문자 무시 비교
    equal := maps.EqualFunc(m1, m2, func(v1, v2 string) bool {
        return strings.EqualFold(v1, v2)
    })
    fmt.Println(equal)  // true
}

3. EqualFunc - 타입 변환 비교

import "strconv"

func main() {
    m1 := map[string]int{
        "a": 1,
        "b": 2,
    }
    
    m2 := map[string]string{
        "a": "1",
        "b": "2",
    }
    
    // int와 string 비교
    equal := maps.EqualFunc(m1, m2, func(v1 int, v2 string) bool {
        return strconv.Itoa(v1) == v2
    })
    fmt.Println(equal)  // true
}

수정 함수

1. DeleteFunc - 조건부 삭제

func main() {
    m := map[string]int{
        "a": 1,
        "b": 2,
        "c": 3,
        "d": 4,
        "e": 5,
    }
    
    // 짝수 값 삭제
    maps.DeleteFunc(m, func(k string, v int) bool {
        return v%2 == 0
    })
    
    fmt.Println(m)  // map[a:1 c:3 e:5]
}

2. DeleteFunc - 키 조건

import "strings"

func main() {
    m := map[string]int{
        "apple":  1,
        "banana": 2,
        "apricot": 3,
        "cherry": 4,
    }
    
    // 'a'로 시작하는 키 삭제
    maps.DeleteFunc(m, func(k string, v int) bool {
        return strings.HasPrefix(k, "a")
    })
    
    fmt.Println(m)  // map[banana:2 cherry:4]
}

3. Insert - 맵 삽입

func main() {
    m := map[string]int{
        "a": 1,
        "b": 2,
    }
    
    // Iterator에서 삽입
    toInsert := map[string]int{
        "c": 3,
        "d": 4,
    }
    
    maps.Insert(m, maps.All(toInsert))
    fmt.Println(m)  // map[a:1 b:2 c:3 d:4]
}

4. Insert - 덮어쓰기

func main() {
    m := map[string]int{
        "a": 1,
        "b": 2,
    }
    
    // 기존 키가 있으면 덮어씀
    updates := map[string]int{
        "a": 100,
        "c": 3,
    }
    
    maps.Insert(m, maps.All(updates))
    fmt.Println(m)  // map[a:100 b:2 c:3]
}

실전 예제

1. 맵 필터링

func FilterMap[K comparable, V any](m map[K]V, pred func(K, V) bool) map[K]V {
    result := make(map[K]V)
    
    for k, v := range maps.All(m) {
        if pred(k, v) {
            result[k] = v
        }
    }
    
    return result
}

func main() {
    scores := map[string]int{
        "Alice": 85,
        "Bob":   92,
        "Carol": 78,
        "Dave":  95,
    }
    
    // 90점 이상만
    highScores := FilterMap(scores, func(name string, score int) bool {
        return score >= 90
    })
    
    fmt.Println(highScores)  // map[Bob:92 Dave:95]
}

2. 맵 변환

func MapValues[K comparable, V1, V2 any](m map[K]V1, f func(V1) V2) map[K]V2 {
    result := make(map[K]V2, len(m))
    
    for k, v := range maps.All(m) {
        result[k] = f(v)
    }
    
    return result
}

func main() {
    prices := map[string]float64{
        "apple":  1.5,
        "banana": 0.8,
        "cherry": 2.0,
    }
    
    // 10% 할인
    discounted := MapValues(prices, func(price float64) float64 {
        return price * 0.9
    })
    
    fmt.Println(discounted)
    // map[apple:1.35 banana:0.72 cherry:1.8]
}

3. 맵 역전 (Key ↔ Value)

func InvertMap[K, V comparable](m map[K]V) map[V]K {
    result := make(map[V]K, len(m))
    
    for k, v := range maps.All(m) {
        result[v] = k
    }
    
    return result
}

func main() {
    original := map[string]int{
        "one":   1,
        "two":   2,
        "three": 3,
    }
    
    inverted := InvertMap(original)
    fmt.Println(inverted)  // map[1:one 2:two 3:three]
}

4. 맵 그룹화

func GroupBy[K comparable, V any](items []V, keyFunc func(V) K) map[K][]V {
    result := make(map[K][]V)
    
    for _, item := range items {
        key := keyFunc(item)
        result[key] = append(result[key], item)
    }
    
    return result
}

type Student struct {
    Name  string
    Grade int
}

func main() {
    students := []Student{
        {"Alice", 90},
        {"Bob", 85},
        {"Carol", 90},
        {"Dave", 85},
        {"Eve", 95},
    }
    
    // 성적별 그룹화
    byGrade := GroupBy(students, func(s Student) int {
        return s.Grade
    })
    
    for grade, studs := range byGrade {
        fmt.Printf("Grade %d: ", grade)
        for _, s := range studs {
            fmt.Printf("%s ", s.Name)
        }
        fmt.Println()
    }
    // Grade 85: Bob Dave
    // Grade 90: Alice Carol
    // Grade 95: Eve
}

5. 맵 병합 (충돌 해결)

func MergeWith[K comparable, V any](
    m1, m2 map[K]V,
    resolve func(V, V) V,
) map[K]V {
    result := maps.Clone(m1)
    
    for k, v2 := range maps.All(m2) {
        if v1, exists := result[k]; exists {
            result[k] = resolve(v1, v2)
        } else {
            result[k] = v2
        }
    }
    
    return result
}

func main() {
    m1 := map[string]int{"a": 1, "b": 2}
    m2 := map[string]int{"b": 3, "c": 4}
    
    // 값 합산
    merged := MergeWith(m1, m2, func(v1, v2 int) int {
        return v1 + v2
    })
    
    fmt.Println(merged)  // map[a:1 b:5 c:4]
    
    // 최댓값 선택
    m3 := map[string]int{"x": 10, "y": 5}
    m4 := map[string]int{"y": 8, "z": 3}
    
    maxMerged := MergeWith(m3, m4, func(v1, v2 int) int {
        if v1 > v2 {
            return v1
        }
        return v2
    })
    
    fmt.Println(maxMerged)  // map[x:10 y:8 z:3]
}

6. 맵 차집합

func Difference[K comparable, V any](m1, m2 map[K]V) map[K]V {
    result := make(map[K]V)
    
    for k, v := range maps.All(m1) {
        if _, exists := m2[k]; !exists {
            result[k] = v
        }
    }
    
    return result
}

func main() {
    m1 := map[string]int{"a": 1, "b": 2, "c": 3}
    m2 := map[string]int{"b": 2, "d": 4}
    
    diff := Difference(m1, m2)
    fmt.Println(diff)  // map[a:1 c:3]
}

7. 맵 교집합

func Intersection[K, V comparable](m1, m2 map[K]V) map[K]V {
    result := make(map[K]V)
    
    for k, v1 := range maps.All(m1) {
        if v2, exists := m2[k]; exists && v1 == v2 {
            result[k] = v1
        }
    }
    
    return result
}

func main() {
    m1 := map[string]int{"a": 1, "b": 2, "c": 3}
    m2 := map[string]int{"b": 2, "c": 4, "d": 5}
    
    inter := Intersection(m1, m2)
    fmt.Println(inter)  // map[b:2] (값도 같아야 함)
}

8. 캐시 구현

import (
    "sync"
    "time"
)

type CacheItem[V any] struct {
    Value      V
    Expiration time.Time
}

type Cache[K comparable, V any] struct {
    items map[K]CacheItem[V]
    mu    sync.RWMutex
}

func NewCache[K comparable, V any]() *Cache[K, V] {
    c := &Cache[K, V]{
        items: make(map[K]CacheItem[V]),
    }
    
    // 주기적 정리
    go c.cleanup()
    
    return c
}

func (c *Cache[K, V]) Set(key K, value V, ttl time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    c.items[key] = CacheItem[V]{
        Value:      value,
        Expiration: time.Now().Add(ttl),
    }
}

func (c *Cache[K, V]) Get(key K) (V, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    
    item, exists := c.items[key]
    if !exists {
        var zero V
        return zero, false
    }
    
    if time.Now().After(item.Expiration) {
        var zero V
        return zero, false
    }
    
    return item.Value, true
}

func (c *Cache[K, V]) cleanup() {
    ticker := time.NewTicker(1 * time.Minute)
    defer ticker.Stop()
    
    for range ticker.C {
        c.mu.Lock()
        
        now := time.Now()
        maps.DeleteFunc(c.items, func(k K, item CacheItem[V]) bool {
            return now.After(item.Expiration)
        })
        
        c.mu.Unlock()
    }
}

func (c *Cache[K, V]) Len() int {
    c.mu.RLock()
    defer c.mu.RUnlock()
    return len(c.items)
}

func main() {
    cache := NewCache[string, int]()
    
    cache.Set("a", 1, 5*time.Second)
    cache.Set("b", 2, 10*time.Second)
    
    if val, ok := cache.Get("a"); ok {
        fmt.Println("a:", val)  // a: 1
    }
    
    time.Sleep(6 * time.Second)
    
    if _, ok := cache.Get("a"); !ok {
        fmt.Println("a expired")  // a expired
    }
}

일반적인 실수

1. Clone의 얕은 복사 오해

// ❌ 나쁜 예 (깊은 복사 기대)
func main() {
    type Data struct {
        Value int
    }
    
    original := map[string]*Data{
        "a": {Value: 1},
    }
    
    cloned := maps.Clone(original)
    
    // 같은 객체를 참조
    original["a"].Value = 100
    fmt.Println(cloned["a"].Value)  // 100 (변경됨!)
}

// ✅ 좋은 예 (수동 깊은 복사)
func DeepClone(original map[string]*Data) map[string]*Data {
    result := make(map[string]*Data, len(original))
    
    for k, v := range maps.All(original) {
        result[k] = &Data{Value: v.Value}
    }
    
    return result
}

2. Copy 방향 혼동

// ❌ 나쁜 예 (인자 순서 혼동)
func main() {
    src := map[string]int{"a": 1}
    dst := map[string]int{"b": 2}
    
    // src로 복사된다고 착각
    maps.Copy(src, dst)
    
    fmt.Println(src)  // map[a:1 b:2] (src가 수정됨)
}

// ✅ 좋은 예 (명확한 의도)
func main() {
    src := map[string]int{"a": 1}
    dst := make(map[string]int)
    
    // dst에 src 복사
    maps.Copy(dst, src)
    
    fmt.Println(dst)  // map[a:1]
}

3. Equal과 참조 비교 혼동

// ❌ 나쁜 예 (같은 맵이어야 한다고 착각)
func main() {
    m1 := map[string]int{"a": 1}
    m2 := map[string]int{"a": 1}
    
    // 참조 비교
    if m1 == m2 {  // 컴파일 에러: invalid operation
        fmt.Println("Equal")
    }
}

// ✅ 좋은 예 (값 비교)
func main() {
    m1 := map[string]int{"a": 1}
    m2 := map[string]int{"a": 1}
    
    if maps.Equal(m1, m2) {
        fmt.Println("Equal")  // Equal
    }
}

4. DeleteFunc 조건 오류

// ❌ 나쁜 예 (삭제 조건 반대)
func main() {
    m := map[string]int{"a": 1, "b": 2, "c": 3}
    
    // "남길" 조건을 잘못 사용
    maps.DeleteFunc(m, func(k string, v int) bool {
        return v > 1  // 2, 3을 삭제
    })
    
    fmt.Println(m)  // map[a:1] (의도와 다름)
}

// ✅ 좋은 예 (명확한 조건)
func main() {
    m := map[string]int{"a": 1, "b": 2, "c": 3}
    
    // 1보다 큰 값을 "유지"하려면 반대로
    maps.DeleteFunc(m, func(k string, v int) bool {
        return v <= 1  // 1 이하를 삭제
    })
    
    fmt.Println(m)  // map[b:2 c:3]
}

5. nil 맵에 Insert

// ❌ 나쁜 예 (nil 맵에 삽입)
func main() {
    var m map[string]int
    
    src := map[string]int{"a": 1}
    maps.Insert(m, maps.All(src))  // 패닉!
}

// ✅ 좋은 예 (맵 초기화)
func main() {
    m := make(map[string]int)
    
    src := map[string]int{"a": 1}
    maps.Insert(m, maps.All(src))
    
    fmt.Println(m)  // map[a:1]
}

6. EqualFunc 파라미터 순서

// ❌ 나쁜 예 (파라미터 순서 혼동)
func main() {
    m1 := map[string]int{"a": 1}
    m2 := map[string]string{"a": "1"}
    
    // v1은 int, v2는 string
    equal := maps.EqualFunc(m1, m2, func(v1 string, v2 int) bool {
        // 타입 에러!
        return v1 == strconv.Itoa(v2)
    })
}

// ✅ 좋은 예 (올바른 타입 순서)
func main() {
    m1 := map[string]int{"a": 1}
    m2 := map[string]string{"a": "1"}
    
    equal := maps.EqualFunc(m1, m2, func(v1 int, v2 string) bool {
        return strconv.Itoa(v1) == v2
    })
    
    fmt.Println(equal)  // true
}

7. Keys/Values 순서 기대

// ❌ 나쁜 예 (순서 보장 기대)
func main() {
    m := map[string]int{"a": 1, "b": 2, "c": 3}
    
    keys := make([]string, 0)
    for k := range maps.Keys(m) {
        keys = append(keys, k)
    }
    
    // 순서가 매번 다를 수 있음
    fmt.Println(keys)
}

// ✅ 좋은 예 (명시적 정렬)
import "sort"

func main() {
    m := map[string]int{"a": 1, "b": 2, "c": 3}
    
    keys := make([]string, 0, len(m))
    for k := range maps.Keys(m) {
        keys = append(keys, k)
    }
    
    sort.Strings(keys)
    fmt.Println(keys)  // [a b c] (정렬됨)
}

베스트 프랙티스

1. 용량 사전 할당

// ✅ 크기를 알 때 사전 할당
func FilterLargeMap(m map[string]int, threshold int) map[string]int {
    // 최대 크기로 할당
    result := make(map[string]int, len(m))
    
    for k, v := range maps.All(m) {
        if v > threshold {
            result[k] = v
        }
    }
    
    return result
}

2. Clone으로 불변성 보장

// ✅ 원본 보호
func ProcessMap(m map[string]int) map[string]int {
    // 원본 보호를 위해 복사
    working := maps.Clone(m)
    
    // working 수정
    maps.DeleteFunc(working, func(k string, v int) bool {
        return v < 0
    })
    
    return working
}

3. EqualFunc로 커스텀 비교

// ✅ 유연한 비교
type Product struct {
    ID    int
    Price float64
}

func CompareProductMaps(m1, m2 map[string]Product) bool {
    return maps.EqualFunc(m1, m2, func(p1, p2 Product) bool {
        // ID만 비교 (Price 무시)
        return p1.ID == p2.ID
    })
}

4. DeleteFunc로 정리

// ✅ 조건부 정리
type Session struct {
    UserID    string
    ExpiresAt time.Time
}

func CleanupSessions(sessions map[string]Session) {
    now := time.Now()
    
    maps.DeleteFunc(sessions, func(id string, s Session) bool {
        return now.After(s.ExpiresAt)
    })
}

5. 제네릭 유틸리티 작성

// ✅ 재사용 가능한 함수
func PickKeys[K comparable, V any](m map[K]V, keys []K) map[K]V {
    result := make(map[K]V, len(keys))
    
    for _, k := range keys {
        if v, exists := m[k]; exists {
            result[k] = v
        }
    }
    
    return result
}

func main() {
    m := map[string]int{"a": 1, "b": 2, "c": 3, "d": 4}
    picked := PickKeys(m, []string{"a", "c", "e"})
    
    fmt.Println(picked)  // map[a:1 c:3]
}

6. 에러 처리

// ✅ 안전한 접근
func SafeMerge[K comparable, V any](
    m1, m2 map[K]V,
) (map[K]V, error) {
    if m1 == nil || m2 == nil {
        return nil, fmt.Errorf("nil map provided")
    }
    
    result := maps.Clone(m1)
    maps.Copy(result, m2)
    
    return result, nil
}

7. 문서화

// ✅ 명확한 문서화
// MergeUnique merges two maps, returning an error if any key exists in both.
// Time complexity: O(n+m) where n and m are the sizes of the input maps.
func MergeUnique[K comparable, V any](
    m1, m2 map[K]V,
) (map[K]V, error) {
    result := maps.Clone(m1)
    
    for k, v := range maps.All(m2) {
        if _, exists := result[k]; exists {
            return nil, fmt.Errorf("duplicate key: %v", k)
        }
        result[k] = v
    }
    
    return result, nil
}

8. 테스트 작성

func TestFilterMap(t *testing.T) {
    tests := []struct {
        name  string
        input map[string]int
        pred  func(string, int) bool
        want  map[string]int
    }{
        {
            name:  "filter even values",
            input: map[string]int{"a": 1, "b": 2, "c": 3, "d": 4},
            pred: func(k string, v int) bool {
                return v%2 == 0
            },
            want: map[string]int{"b": 2, "d": 4},
        },
        {
            name:  "empty map",
            input: map[string]int{},
            pred: func(k string, v int) bool {
                return true
            },
            want: map[string]int{},
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := FilterMap(tt.input, tt.pred)
            
            if !maps.Equal(got, tt.want) {
                t.Errorf("got %v, want %v", got, tt.want)
            }
        })
    }
}

정리

  • 기본: All, Keys, Values로 다양한 순회, Collect로 변환
  • 복사: Clone (얕은 복사), Copy (병합)
  • 비교: Equal (동등), EqualFunc (커스텀)
  • 수정: DeleteFunc (조건 삭제), Insert (삽입)
  • 실전: 필터링, 변환, 역전, 그룹화, 병합, 차집합, 교집합, 캐시
  • 실수: Clone 얕은 복사, Copy 방향, Equal vs ==, DeleteFunc 조건, nil 맵, EqualFunc 순서, Keys 순서
  • 베스트: 용량 할당, Clone 불변성, EqualFunc 활용, DeleteFunc 정리, 제네릭, 에러 처리, 문서화, 테스트