No history yet

Слайсы и карты

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

В Go слайс — это не просто динамический массив. Это легковесная структура-обертка, которая предоставляет вид на часть базового массива. Сама структура слайса, которую еще называют заголовком, состоит всего из трех полей:

  1. Указатель (pointer) на первый элемент в базовом массиве, к которому у слайса есть доступ.
  2. Длина (length) — количество элементов в слайсе. Это то, что возвращает функция len().
  3. Емкость (capacity) — количество элементов в базовом массиве, начиная от указателя слайса и до конца массива. Это то, что возвращает функция cap().

Когда вы передаете слайс в функцию, вы копируете только этот маленький заголовок, а не весь базовый массив. Именно поэтому изменения, внесенные в элементы слайса внутри функции, видны снаружи — ведь и оригинальный, и скопированный заголовки указывают на одни и те же данные в памяти.

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

Для создания слайса с заранее определенной длиной и емкостью используется встроенная функция make. Это предпочтительный способ, если вы примерно знаете, сколько элементов будете хранить. Заранее выделяя память, вы избегаете лишних аллокаций в будущем.

// Создаем слайс с длиной 0, но с емкостью 10.
// Мы готовы добавить 10 элементов без переаллокации.
numbers := make([]int, 0, 10)

Самая частая операция со слайсами — добавление элементов с помощью функции append. Её механика напрямую зависит от емкости. Если при добавлении нового элемента остается место в базовом массиве (len < cap), элемент просто добавляется, а длина слайса увеличивается. Если же места нет (len == cap), происходит переаллокация: Go создает новый, более крупный массив, копирует в него все старые элементы, добавляет новый, а затем заголовок слайса обновляется, чтобы указывать на этот новый массив. Старый массив, если на него больше нет ссылок, будет очищен сборщиком мусора.

Важно помнить, что append всегда возвращает новый заголовок слайса, так как длина, емкость и даже указатель могут измениться. Поэтому правильное использование append всегда выглядит так: mySlice = append(mySlice, newValue).

Начиная с версии Go 1.22, в стандартной библиотеке появилась функция slices.Concat, которая упрощает объединение нескольких слайсов в один. Раньше для этого приходилось использовать append в цикле, что было менее читабельно.

import "slices"

s1 := []int{1, 2}
s2 := []int{3, 4}

// Новый способ в Go 1.22+
result := slices.Concat(s1, s2) 
// result теперь {1, 2, 3, 4}

Карты: неупорядоченные коллекции

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

Инициализировать карту нужно с помощью make. Работа с nil-картой (неинициализированной) приведет к панике при попытке записи. Однако чтение из nil-карты безопасно — оно всегда вернет нулевое значение для типа-значения.

// Инициализация карты, где ключ - строка, а значение - целое число.
ages := make(map[string]int)

ages["Alice"] = 30
ages["Bob"] = 25

Одна из ключевых идиом в Go — проверка наличия ключа в карте. При обращении к карте можно получить два значения: само значение и булеву переменную, которая будет true, если ключ существует, и false в противном случае. Это позволяет отличить реально хранящийся ноль от отсутствия ключа.

// age будет 0, а ok будет false, т.к. ключа "Charlie" нет.
age, ok := ages["Charlie"]
if !ok {
    fmt.Println("Charlie's age is unknown.")
}

// Удаление ключа
delete(ages, "Bob")

Перебор карты осуществляется с помощью цикла for range. Важно помнить, что порядок итерации по карте не гарантирован и может меняться от запуска к запуску. Если вам нужен стабильный порядок, необходимо сначала извлечь ключи в слайс, отсортировать его, а затем итерироваться по слайсу, получая значения из карты.

Новое в циклах for range

До версии Go 1.22 существовала распространенная ловушка, связанная с циклами for range. Переменные, объявленные в цикле, переиспользовались на каждой итерации. Если вы пытались получить адрес такой переменной (например, в ), все ссылки указывали на значение последней итерации.

// Старое поведение (до Go 1.22)
numbers := []int{10, 20, 30}
var pointers []*int

for _, v := range numbers { 
    // На каждой итерации v - это одна и та же переменная,
    // в которую просто записывается новое значение.
    pointers = append(pointers, &v)
}

// Все указатели в 'pointers' будут указывать на 30!

Начиная с Go 1.22, это поведение изменили. Теперь переменная цикла создается заново на каждой итерации, что полностью решает эту проблему. Ваш код становится интуитивно понятнее и безопаснее. Кроме того, в for range теперь можно передавать целое число для итерации от 0 до n-1.

// Новое поведение (Go 1.22+)
numbers := []int{10, 20, 30}
var pointers []*int

for _, v := range numbers {
    // Теперь 'v' - это новая переменная на каждой итерации.
    pointers = append(pointers, &v)
}

// Указатели будут указывать на 10, 20 и 30, как и ожидалось.

// Новая возможность итерации по числу:
for i := range 5 {
    fmt.Println(i) // Напечатает 0, 1, 2, 3, 4
}
Quiz Questions 1/5

Из каких трех полей состоит заголовок слайса (slice header) в Go?

Quiz Questions 2/5

Что произойдет, если вызвать append для слайса, у которого длина равна емкости (len == cap)?

Теперь вы знаете, как слайсы и карты устроены «под капотом» и какие идиомы использовать для написания эффективного и безопасного кода на Go.