Карта (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)
}