MidТеория6 min

Карты (Maps)

Хэш-таблицы в Go: создание, операции, итерация, внутреннее устройство, sync.Map и паттерны

Карта (map) в Go — это встроенная реализация хэш-таблицы, обеспечивающая связь ключ-значение с амортизированным O(1) доступом.

Создание карт

package main

import "fmt"

func main() {
    // 1. Map literal
    colors := map[string]string{
        "red":   "#FF0000",
        "green": "#00FF00",
        "blue":  "#0000FF",
    }

    // 2. make() — empty map with optional capacity hint
    scores := make(map[string]int)
    scores["Alice"] = 95
    scores["Bob"] = 87

    // 3. make() with capacity hint (optimization, not a limit)
    largeMap := make(map[int]string, 1000)
    _ = largeMap

    // 4. nil map (var declaration)
    var nilMap map[string]int
    // nilMap is nil, len=0

    // 5. Empty map (not nil)
    emptyMap := map[string]int{}
    // emptyMap is NOT nil, len=0

    fmt.Println(colors)
    fmt.Println(scores)
    fmt.Println(nilMap == nil)   // true
    fmt.Println(emptyMap == nil) // false
}

Требования к типу ключа

Ключи карты должны быть сравнимыми (comparable) — поддерживать оператор ==:

Можно использовать как ключ Нельзя использовать как ключ
int, float64, string []int (слайсы)
bool, byte, rune map[K]V (карты)
struct (если все поля comparable) func (функции)
[N]T (массивы, если T comparable) struct с несравнимыми полями
Указатели *T —
Интерфейсы (runtime panic если не comparable) —

CRUD-операции

package main

import "fmt"

func main() {
    m := make(map[string]int)

    // CREATE / UPDATE — same syntax
    m["Alice"] = 95     // Create
    m["Bob"] = 87       // Create
    m["Alice"] = 98     // Update (overwrite)

    // READ
    score := m["Alice"]
    fmt.Println(score) // 98

    // READ with comma-ok idiom — distinguish zero value from missing key
    score, ok := m["Alice"]
    fmt.Println(score, ok) // 98 true

    score, ok = m["Charlie"]
    fmt.Println(score, ok) // 0 false — key doesn't exist

    // Common pattern
    if score, ok := m["Alice"]; ok {
        fmt.Printf("Alice's score: %d\n", score)
    } else {
        fmt.Println("Alice not found")
    }

    // DELETE
    delete(m, "Bob")    // Remove key "Bob"
    delete(m, "Nobody") // No-op if key doesn't exist (no error)

    fmt.Println(m)      // map[Alice:98]
    fmt.Println(len(m)) // 1

    // clear() — remove all entries (Go 1.21+)
    clear(m)
    fmt.Println(m)      // map[]
    fmt.Println(len(m)) // 0
}

nil map vs empty map

package main

import "fmt"

func main() {
    var nilMap map[string]int    // nil
    emptyMap := map[string]int{} // not nil

    // READ from nil map — OK, returns zero value
    fmt.Println(nilMap["key"]) // 0

    // len() on nil map — OK, returns 0
    fmt.Println(len(nilMap)) // 0

    // WRITE to nil map — PANIC!
    // nilMap["key"] = 1 // panic: assignment to entry in nil map

    // WRITE to empty map — OK
    emptyMap["key"] = 1 // Works fine

    // delete from nil map — OK, no-op
    delete(nilMap, "key") // No panic

    // range over nil map — OK, zero iterations
    for k, v := range nilMap {
        fmt.Println(k, v) // Never executes
    }
}
Операция nil map empty map
Read m["key"] OK (zero value) OK (zero value)
Write m["key"] = v PANIC OK
delete(m, "key") OK (no-op) OK
len(m) 0 0
range m 0 iterations 0 iterations
m == nil true false

Итерация по карте

package main

import (
    "fmt"
    "sort"
)

func main() {
    m := map[string]int{
        "Go":     2009,
        "Rust":   2010,
        "Python": 1991,
        "Java":   1995,
    }

    // Iterate over key-value pairs
    for key, value := range m {
        fmt.Printf("%s: %d\n", key, value)
    }
    // WARNING: iteration order is RANDOM!
    // Each run may produce different order

    // Iterate over keys only
    for key := range m {
        fmt.Println(key)
    }

    // Iterate over values only (Go 1.22+)
    for _, value := range m {
        fmt.Println(value)
    }

    // Sorted iteration — collect keys, sort, then iterate
    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])
    }
}

Важно: Порядок итерации по карте в Go намеренно рандомизирован. Это сделано чтобы код не зависел от конкретного порядка. Если нужен отсортированный вывод, соберите ключи в слайс и отсортируйте.

Внутреннее устройство

Карты в Go реализованы как хэш-таблица с бакетами (buckets):

map[string]int
├── Hash function
├── Array of buckets (8 entries each)
│   ├── Bucket 0: [tophash][keys][values][overflow*]
│   ├── Bucket 1: [tophash][keys][values][overflow*]
│   ├── Bucket 2: ...
│   └── ...
└── Overflow buckets (linked list for collisions)

Ключевые характеристики:

Аспект Описание
Bucket size 8 пар ключ-значение на бакет
Хэш-функция Зависит от типа ключа, seed рандомизирован
Load factor При заполнении ~6.5 элементов/бакет происходит grow
Growing Инкрементальный rehash (не останавливает мир)
Потокобезопасность НЕТ — нужна внешняя синхронизация
Указатель Map — reference type (как слайс, но без len/cap в header)

Map — reference type

func modify(m map[string]int) {
    m["added"] = 100 // Modifies the original!
}

func main() {
    m := map[string]int{"x": 1}
    modify(m)
    fmt.Println(m) // map[added:100 x:1]
}

Карта — ссылочный тип. Передача в функцию не создаёт копию данных.

sync.Map — потокобезопасная карта

Обычная карта Go не потокобезопасна. Для конкурентного доступа используйте sync.Map или sync.RWMutex:

Вариант 1: sync.RWMutex (чаще предпочтительнее)

package main

import (
    "fmt"
    "sync"
)

type SafeMap struct {
    mu sync.RWMutex
    m  map[string]int
}

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

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

func (sm *SafeMap) Set(key string, value int) {
    sm.mu.Lock()          // Exclusive access for writing
    defer sm.mu.Unlock()
    sm.m[key] = value
}

func (sm *SafeMap) Delete(key string) {
    sm.mu.Lock()
    defer sm.mu.Unlock()
    delete(sm.m, key)
}

func main() {
    sm := NewSafeMap()
    sm.Set("counter", 1)
    if val, ok := sm.Get("counter"); ok {
        fmt.Println(val) // 1
    }
}

Вариант 2: sync.Map (для специфических сценариев)

package main

import (
    "fmt"
    "sync"
)

func main() {
    var m sync.Map

    // Store
    m.Store("key1", "value1")
    m.Store("key2", 42)

    // Load
    if val, ok := m.Load("key1"); ok {
        fmt.Println(val.(string)) // Type assertion needed
    }

    // LoadOrStore — load existing or store new
    actual, loaded := m.LoadOrStore("key3", "new-value")
    fmt.Println(actual, loaded) // "new-value" false

    actual, loaded = m.LoadOrStore("key1", "other")
    fmt.Println(actual, loaded) // "value1" true (existing value)

    // Delete
    m.Delete("key2")

    // Range
    m.Range(func(key, value any) bool {
        fmt.Printf("%v: %v\n", key, value)
        return true // Continue iteration
    })

    // LoadAndDelete (Go 1.15+)
    val, loaded := m.LoadAndDelete("key1")
    fmt.Println(val, loaded) // "value1" true

    // Swap (Go 1.20+)
    // CompareAndSwap, CompareAndDelete (Go 1.20+)
}

Когда использовать sync.Map vs RWMutex

Сценарий Рекомендация
Ключи записываются один раз, читаются много sync.Map
Частые записи и чтения sync.RWMutex + обычная map
Непересекающиеся наборы ключей у горутин sync.Map
Нужна типизация sync.RWMutex + обычная map
Простота кода sync.RWMutex + обычная map

Паттерн: Map как Set

Go не имеет встроенного типа Set. Карта с struct{} в качестве значения — эффективная замена:

package main

import "fmt"

// Set using map[T]struct{} — zero memory for values
type StringSet map[string]struct{}

func NewStringSet(items ...string) StringSet {
    s := make(StringSet, len(items))
    for _, item := range items {
        s[item] = struct{}{}
    }
    return s
}

func (s StringSet) Add(item string) {
    s[item] = struct{}{}
}

func (s StringSet) Remove(item string) {
    delete(s, item)
}

func (s StringSet) Contains(item string) bool {
    _, ok := s[item]
    return ok
}

func (s StringSet) Len() int {
    return len(s)
}

// Union of two sets
func (s StringSet) Union(other StringSet) StringSet {
    result := NewStringSet()
    for k := range s {
        result.Add(k)
    }
    for k := range other {
        result.Add(k)
    }
    return result
}

// Intersection of two sets
func (s StringSet) Intersection(other StringSet) StringSet {
    result := NewStringSet()
    for k := range s {
        if other.Contains(k) {
            result.Add(k)
        }
    }
    return result
}

func main() {
    fruits := NewStringSet("apple", "banana", "cherry")
    berries := NewStringSet("cherry", "strawberry", "blueberry")

    fmt.Println(fruits.Contains("apple"))  // true
    fmt.Println(fruits.Contains("mango"))  // false

    union := fruits.Union(berries)
    fmt.Println(union.Len()) // 5

    inter := fruits.Intersection(berries)
    fmt.Println(inter.Contains("cherry")) // true
    fmt.Println(inter.Len())              // 1
}

Почему struct{} а не bool? struct{} занимает 0 байт памяти, в то время как bool занимает 1 байт. Для множества из миллиона элементов это экономит 1 МБ.

Полезные паттерны

Подсчёт частоты (Word Count)

func wordCount(text string) map[string]int {
    counts := make(map[string]int)
    words := strings.Fields(text)
    for _, word := range words {
        counts[strings.ToLower(word)]++
    }
    return counts
}

Группировка

func groupBy(items []Item, keyFn func(Item) string) map[string][]Item {
    groups := make(map[string][]Item)
    for _, item := range items {
        key := keyFn(item)
        groups[key] = append(groups[key], item)
    }
    return groups
}

Мемоизация

func memoize(fn func(int) int) func(int) int {
    cache := make(map[int]int)
    return func(n int) int {
        if result, ok := cache[n]; ok {
            return result
        }
        result := fn(n)
        cache[n] = result
        return result
    }
}

Предварительное выделение ёмкости

// GOOD: hint the expected size
users := make(map[int]*User, len(userIDs))
for _, id := range userIDs {
    users[id] = fetchUser(id)
}

// BAD: no size hint — will rehash multiple times
users := make(map[int]*User)
for _, id := range userIDs {
    users[id] = fetchUser(id)
}

Проверь себя

Как отличить отсутствующий ключ от ключа с нулевым значением?

Гарантирован ли порядок итерации по карте в Go?

Является ли встроенная карта Go потокобезопасной?

Какой тип значения наиболее эффективен для реализации множества (Set) через map?

Что произойдёт при записи в `nil` карту?

Code Challenges

Подсчёт частоты слов

Напишите функцию, которая подсчитывает частоту каждого слова в строке. Слова разделены пробелами, без учёта регистра.

Test Cases

1. Input: hello world hello→ Expected: {"hello":2,"world":1}
2. Input: Go go GO→ Expected: {"go":3}
3.→ Expected: {}