Массивы, слайсы и карты
Добро пожаловать на пятый урок! Теперь мы переходим к работе с данными. В Go три основные структуры для хранения коллекций:
- Массивы — фиксированного размера (редко используются напрямую)
- Слайсы — динамические массивы (самая популярная структура в Go!)
- Карты (map) — хэш-таблицы, пары ключ-значение
Понимание этих трёх вещей — ключ к написанию настоящего Go-кода. Слайсы и карты используются повсюду.
Массивы
Массив — это фиксированная последовательность элементов одного типа. Размер массива известен на этапе компиляции и не меняется.
Объявление и инициализация
// Явное объявление
var scores [5]int // [0 0 0 0 0]
// С инициализацией
numbers := [5]int{10, 20, 30, 40, 50}
// Go сам посчитает размер
names := [...]string{"Алиса", "Боб", "Вика"}
// Инициализация по индексу
grades := [5]int{0: 95, 2: 88, 4: 100} // [95 0 88 0 100]
Работа с массивами
func main() {
arr := [4]int{1, 2, 3, 4}
fmt.Println("Первый элемент:", arr[0])
arr[1] = 99
fmt.Println("После изменения:", arr)
fmt.Println("Длина:", len(arr)) // всегда 4
// Перебор
for i, v := range arr {
fmt.Printf("arr[%d] = %d\n", i, v)
}
}
Многомерные массивы
matrix := [3][3]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
Важно: массивы — это значения. При присваивании копируется весь массив.
a := [3]int{1, 2, 3}
b := a
b[0] = 99
fmt.Println(a) // [1 2 3] — a не изменился!
Массивы полезны, когда размер строго фиксирован (например, координаты [x y z], RGB-цвета).
Слайсы — звёзды Go
Представь обычный массив: ты говоришь «хочу 10 чисел», создаёшь [10]int, и всё — размер зафиксирован навсегда. Хочешь добавить 11-е? Придётся создавать новый массив и копировать всё вручную. Скучно и хлопотно.
А теперь — слайс (slice). Это как умный, живой взгляд на массив: он знает, где лежат данные, сколько их сейчас и сколько места зарезервировано под будущее расширение.
Слайс — это маленькая структура из трёх полей под капотом:
- ptr — указатель на реальный массив в памяти
- len — текущая длина (сколько элементов можно использовать)
- cap — ёмкость (сколько всего места выделено под массив)
Создание слайсов
// Пустой (nil) слайс
var nums []int
// Через make
scores := make([]int, 5) // длина 5, ёмкость 5, заполнен 0
names := make([]string, 0, 10) // длина 0, ёмкость 10
// Литерал (самый удобный способ)
fruits := []string{"яблоко", "банан", "груша"}
Для простоты используйvar s []intили[]int{}, а для производительности и больших данных —makeс заранее заданной ёмкостью.
Срезы (slicing)
numbers := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// numbers[low:high] — от low включительно до high исключительно
part := numbers[2:6] // [2 3 4 5]
head := numbers[:4] // [0 1 2 3]
tail := numbers[6:] // [6 7 8 9]
all := numbers[:] // копия всего слайса
Осторожно: подслайс делит память с оригиналом!
a := []int{1, 2, 3}
b := a[1:3] // [2 3]
b[0] = 99
fmt.Println(a) // [1 99 3] — изменился и оригинал!
Основные операции
s := []int{1, 2, 3}
// Добавление
s = append(s, 4) // [1 2 3 4]
s = append(s, 5, 6, 7) // [1 2 3 4 5 6 7]
// Добавление другого слайса
more := []int{8, 9}
s = append(s, more...) // [1 2 3 4 5 6 7 8 9]
// Длина и ёмкость
fmt.Println(len(s), cap(s))
Если объявитьvar s []int, то получится nil-слайс (указатель = nil, len=0, cap=0), но к нему спокойно можно делатьappend— Go сам создаст массив и добавит элементы, без паник. Однако каждый раз при нехватке места будет происходить перевыделение памяти, что чуть медленнее. Функцияmake([]int, 0, N)создаёт слайс с уже выделенным местом на N элементов (len=0, но cap=N), поэтому последующиеappend(пока не превысим N) работают быстрее и без лишнего копирования — идеально, когда примерно знаешь будущий размер
Копирование слайсов
src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src) // копирует min(len(dst), len(src)) элементов
fmt.Println(dst) // [1 2 3]
Копировать стоит только тогда, когда тебе действительно нужна независимость: например, при передаче слайса в функцию, где он будет изменён, при сортировке или обработке части данных, или когда возвращаешь слайс из функции, не затрагивая оригинал. Во всех остальных случаях просто делай присваиваниеdst := src— это быстрее, экономит память и является стандартным идиоматичным способом работы со слайсами в Go.
Полезные приёмы
Удаление элемента по индексу:
func removeAt(s []int, i int) []int {
return append(s[:i], s[i+1:]...)
}
Вставка элемента в опрелелённое место:
func insertAt(s []int, i int, v int) []int {
s = append(s, 0) // место для нового
copy(s[i+1:], s[i:]) // сдвигаем
s[i] = v
return s
}
Почему именно так?
appendнужен, чтобы увеличить длину слайса (иначе некуда вставлять).copy— самый эффективный способ сдвинуть элементы вправо (он работает с перекрывающимися областями правильно).- Сначала сдвигаем, потом вставляем — иначе новое значение могло бы быть перезаписано при копировании.
Фильтрация:
func oddOnly(nums []int) []int {
var result []int
for _, n := range nums {
if n%2 == 1 {
result = append(result, n)
}
}
return result
}
Карты (map)
Представь обычный словарь: ищешь слово "транзакция" — открываешь страницу и сразу находишь объяснение. В Go map (карта) — это встроенная хэш-таблица для хранения пар «ключ → значение», где ключи уникальные, а доступ к значению по ключу супербыстрый (почти O(1)). Объявляется так: var m map[string]int — это создаёт nil-map (nil, len=0), к которому нельзя сразу писать, но можно читать (вернёт нулевое значение)

Создание
// Пустая карта
var m map[string]int
// Через make
ages := make(map[string]int)
// Литерал
colors := map[string]string{
"red": "#ff0000",
"green": "#00ff00",
"blue": "#0000ff",
}
Операции
ages["Алиса"] = 25
ages["Боб"] = 30
// Получение
age := ages["Алиса"] // 25
age, ok := ages["Вика"] // 0, false
// Проверка на существование значения по ключу "Боб"
if age, exists := ages["Боб"]; exists {
fmt.Printf("Бобу %d лет\n", age)
}
// Удаление
delete(ages, "Алиса")
// Перебор (порядок случайный!)
for name, age := range ages {
fmt.Printf("%s: %d\n", name, age)
}
Важно: карты — это ссылки. Присваивание копирует только ссылку.
m1 := map[string]int{"x": 1}
m2 := m1
m2["y"] = 2
fmt.Println(m1) // {"x": 1, "y": 2}
Практические примеры
Подсчёт частоты слов
func wordFrequency(text string) map[string]int {
words := strings.Fields(strings.ToLower(text)) // strings - это стандартная библиотека, а метод Fields позволяет разбить строку на слова
freq := make(map[string]int)
for _, w := range words {
freq[w]++
}
return freq
}
Множество (Set) на основе map
Множество (Set) в программировании — это структура данных, которая хранит только уникальные элементы без дубликатов и без фиксированного порядка (элементы не индексируются).
type StringSet map[string]struct{}
func NewStringSet(items ...string) StringSet {
s := make(StringSet)
for _, item := range items {
s[item] = struct{}{}
}
return s
}
func (s StringSet) Add(item string) { s[item] = struct{}{} }
func (s StringSet) Has(item string) bool {
_, exists := s[item]
return exists
}
Со структурами вы познакомитесь в следующем разделе! В этом примере вы могли использовать и типbool, но хорошая практика использоватьstruct{}вместоboolдля экономии памяти т.к.struct{}занимает 0 байт памяти, аboolзанимает 1 байт памяти (Различие малое, но важное!)
Что такое эвакуация данных в map
Эвакуация (evacuation) — это внутренний механизм runtime Go, который перемещает пары ключ-значение из старых бакетов (buckets) мапы в новые при её расширении или сжатии. Это нужно, чтобы мапа оставалась быстрой и эффективной.
Бакеты (buckets) в мапах Go — это маленькие фиксированные массивы (по 8 слотов каждый), в которые runtime помещает пары ключ-значение.
Почему это происходит?
Мапа в Go реализована как хэш-таблица с бакетами. Когда элементов становится слишком много (load factor > 6.5), мапа удваивает количество бакетов, чтобы реже были коллизии. Иногда (при удалении многих элементов) — сжимает.
Коллизия (collision) в контексте мап (хэш-таблиц) — это ситуация, когда два разных ключа имеют одинаковый хэш (или младшие биты хэша совпадают), и поэтому они попадают в один и тот же бакет.
Если просто создать новую большую таблицу и скопировать всё сразу — это будет медленно для больших мап. Поэтому Go использует постепенную (incremental) эвакуацию.
Как map растёт и меняется внутри
Представь, что map — это большая коробка с ячейками, куда ты кладешь вещи по ярлыкам (ключам). Когда вещей становится слишком много, коробка переполняется, и поиск замедляется. Чтобы этого не случилось, Go иногда делает коробку побольше (или поменьше, если вещей стало мало).
В старых версиях Go (до 1.24) при таком «переезде» использовалась эвакуация: Go создавал новую большую коробку рядом со старой и понемногу (по 2 ячейки за раз) переносил вещи при каждом обращении к map. Так не было долгой паузы — всё работало плавно.
А начиная с Go 1.24 (февраль 2025 года) коробку переделали по-новому — теперь это не старые ячейки с цепочками, а умная плоская таблица (Swiss Table). Когда нужно увеличить место, Go не переносит всё постепенно, а просто «раздваивает» перегруженные части таблицы и аккуратно перестраивает только то, что нужно. Это происходит быстрее, использует меньше памяти и работает шустрее даже с миллионами элементов.
Для тебя как программиста ничего не меняется: ты по-прежнему пишешь m["key"] = value, delete(m, "key") и for k, v := range m. Всё работает как раньше, только под капотом стало быстрее и экономнее. Главное — обнови Go до версии 1.24 или новее, и твои карты будут летать ещё быстрее! 🚀