Длина и Ёмкость Слайса в 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.
Подводя итог, длина слайса это число доступных элементов в слайсе, тогда как ёмкость слайса это число элементов в базовом массиве. Добавление элемента в заполненный слайс (длина == ёмкость) приводит к созданию нового базового массива с новой ёмкостью, копированию всех элементов из предыдущего массива и обновлению указателя слайса на новый массив.
Обновлено: 23.07.2025