Перейти к основному содержимому

Массивы, слайсы и карты

Добро пожаловать на пятый урок! Теперь мы переходим к работе с данными. В 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
}

Почему именно так?

  1. append нужен, чтобы увеличить длину слайса (иначе некуда вставлять).
  2. copy — самый эффективный способ сдвинуть элементы вправо (он работает с перекрывающимися областями правильно).
  3. Сначала сдвигаем, потом вставляем — иначе новое значение могло бы быть перезаписано при копировании.

Фильтрация:

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), к которому нельзя сразу писать, но можно читать (вернёт нулевое значение)

Gofer map

Создание

// Пустая карта
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 или новее, и твои карты будут летать ещё быстрее! 🚀