Длина и Ёмкость Слайса в Go

Перевод статьи от автора книги “100 Go Mistakes”

Среди Go разработчиков довольно распространено путать понятия длины (length) и ёмкости (capacity) слайса или не до конца понимать их. В то же время осознание этих двух концепций важно для эффективного использования основных операций связанных со слайсами: инициализация слайса, добавление нового элемента (append), копирование слайсов и формирование среза (подслайса) из основного слайса. Это недопонимание может стать причиной использования слайсов неоптимальным образом или привести к утечкам памяти в программе.

В языке Go слайс формируется на основе массива. Это означает, что данные слайса последовательно хранятся в структуре данных массива. И слайс обрабатывает логику добавления элемента, если базовый массив заполнен.

Под капотом слайс хранит указатель на базовый массив, а также значение своей текущей длины и ёмкости базового массива. Длина слайса - это число элементов, которые находятся в слайсе, тогда как ёмкость - это число элементов в базовом массиве, считая от первого элемента слайса. Давайте рассмотрим несколько примеров, чтобы прояснить эти концепции. Сначала проинициализируем слайс со следующей длиной и ёмкостью:

s := make([]int, 3, 6) // Слайс с длиной три и ёмкостью шесть

Первым обязательным аргументом задаётся длина слайса. Второй аргумент опционален и обозначает ёмкость слайса. На рисунке 1 показан результат работы этого кода в памяти.

В этом случае make создаёт базовый массив из шести элементов (значение ёмкости). Но так как длина слайса равна 3, то Go проинициализировал только первые три элемента. Также, поскольку тип слайса равен []int, то три проинициализированных элемента имеют нулевое значение типа int: 0. Серые элементы аллоцированы, но ещё не использованы.

Если мы напечатаем слайс, то получим элементы в диапазоне длины слайса: [0 0 0]. Если мы установим s[1] = 1, то второй элемент слайса обновится, не влияя на длину и ёмкость слайса. Это показано на рисунке 2. Однако доступ к элементам вне длины слайса запрещён, хотя элементы и выделены в памяти. Например, s[4] = 0 приведёт к панике:

panic: runtime error: index out of range [4] with length 3

Как мы можем использовать свободное место слайса? С помощью встроенной функции append:

s = append(s, 2)

Этот код добавляет к существующему слайсу s новый элемент. Функция использует первый серый элемент (который выделен, но ещё не использован) для хранения элемента со значением 2, это продемонстрировано на рисунке 3.

Длина слайса обновится с 3 до 4, т.к. теперь слайс хранит четыре элемента. Но что произойдёт, если мы добавим ещё три элемента в слайс? Ведь размера базового массива для этого недостаточно.

s = append(s, 3, 4, 5)
fmt.Println(s)

Если мы запустим этот код, то увидим, что слайс изменил свой размер без проблем.

[0 1 0 2 3 4 5]

Из-за того, что массив это структура фиксированного размера, она может хранить элементы только до 4 включительно. Когда мы хотим добавить элемент со значением 5, массив уже полностью заполнен; Go внутренне создаёт новый массив с удвоенной ёмкостью, копирует все элементы предыдущего массива в новый и добавляет элемент 5. Рисунок 4 иллюстрирует данный процесс. Теперь слайс указывает на новый базовый массив. Что произойдёт с предыдущим базовым массивом? Если на него нет ещё указателей и он был выделен в куче, он будет освобождён из памяти с помощью сборщика мусора (GC). (Обсуждение кучи находится в разборе #95, “Не понимание различий стэка и кучи” и смотрим на работу GC в разборе ошибки #99, “Не понимание работы сборщика мусора”).

Что происходит при формировании среза (подслайса)? Срез - операция, выполняемая над массивами или слайсами, предоставляющая полуоткрытый диапазон; первый индекс включается в диапазон, тогда как второй не включается. Следующий пример показывает работу среза и рисунок 5 демонстрирует результат в памяти:

s1 := make([]int, 3, 6) // Слайс с длиной три и ёмкостью шесть
s2 := s1[1:3] // Слайс от индекса 1 до 3


Вначале s1 создаёт слайс с длиной три и ёмкостью шесть. Тогда как, s2 это срез от s1, оба слайса ссылаются на один базовый массив. Однако s2 начинается с другого индекса - 1. Поэтому его длина и ёмкость (длина два и ёмкость пять) отличаются от s1. Если мы обновим s1[1] или s2[0], то изменения произойдут в одном и том же базовом массиве, поэтому будут видны в обоих слайсах, как показано на рисунке 6: Теперь, что произойдёт, если мы добавим новый элемент в s2? Изменит ли следующий код s1?

s2 = append(s2, 2)

Общий базовый массив изменился, но длина слайса изменилась только в s2. Рисунок 7 показывает результат добавления элемента в s2. s1 всё ещё слайс с длинной три и ёмкостью шесть. Поэтому, если мы выведем на экран s1 и s2, то добавленный элемент виден только в s2:

s1=[0 1 0], s2=[1 0 2]

Важно понимать это поведение в слайсах для того, чтобы не сделать неправильных предположений при использовании append.

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

Последний пример, что произойдёт если мы продолжим добавлять элементы в s2 до тех пор пока базовый массив не заполнится? Как это выглядит с точки зрения памяти? Давайте добавим ещё три элемента, так что базовому массиву не хватит ёмкости для их хранения:

s2 = append(s2, 3)
s2 = append(s2, 4) // На этом этапе базовый массив полностью заполнен
s2 = append(s2, 5)

Этот код приводит к созданию другого базового массива. Рисунок 8 демонстрирует результат в памяти: Теперь s1 и s2 указывают на два разных базовых массива. Так как s1 всё ещё слайс с длинной три и ёмкостью шесть, то он всё ещё имеет доступное место в базовом массиве и поэтому продолжает ссылаться на исходный массив. Также новый базовый массив создан путём копирования всех элементов слайса s2, который указывал на индекс 1 базового массива. Поэтому новый базовый массив начинается с элемента 1, но не 0.

Подводя итог, длина слайса это число доступных элементов в слайсе, тогда как ёмкость слайса это число элементов в базовом массиве. Добавление элемента в заполненный слайс (длина == ёмкость) приводит к созданию нового базового массива с новой ёмкостью, копированию всех элементов из предыдущего массива и обновлению указателя слайса на новый массив.


Создано: 16.07.2025
Обновлено: 23.07.2025
Технологии