TL;DR: длина слайса — это количество доступных элементов в слайсе, тогда как емкость слайса — это количество элементов в резервном массиве, считая от первого элемента в срезе.

Разработчики Go довольно часто смешивают длину среза и емкость или не понимают их полностью. Усвоение этих двух концепций необходимо для эффективной обработки основных операций, таких как инициализация среза и добавление элементов с добавлением, копированием или нарезкой. Это недоразумение может привести к неоптимальному использованию слайсов или даже к утечкам памяти.

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

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

Первый аргумент, представляющий длину, является обязательным. Однако второй аргумент, представляющий емкость, является необязательным. На рис. 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 новый элемент. Он использует первый затененный элемент (который был выделен, но еще не использовался) для хранения элемента 2, как показано на рисунке 3.

Длина среза изменена с 3 на 4, поскольку теперь срез содержит четыре элемента. Что произойдет, если мы добавим еще три элемента, чтобы резервный массив стал недостаточно большим?

Если мы запустим этот код, то увидим, что слайс смог справиться с нашим запросом:

[0 1 0 2 3 4 5]

Поскольку массив является структурой фиксированного размера, он может хранить новые элементы до элемента 4. Когда мы хотим вставить элемент 5, массив уже заполнен: Go внутренне создает другой массив, удваивая емкость, копируя все элементы и затем вставляем элемент 5. На рис. 4 показан этот процесс.

Теперь срез ссылается на новый резервный массив. Что произойдет с предыдущим резервным массивом? Если на него больше нет ссылок, он в конечном итоге освобождается сборщиком мусора (GC), если он размещен в куче. (Мы обсуждаем память кучи в ошибке № 95 «Непонимание стека и кучи» и смотрим, как работает сборщик мусора в ошибке № 99 Непонимание того, как работает сборщик мусора.)

Что происходит с нарезкой? Нарезка — это операция, выполняемая над массивом или срезом, обеспечивающая полуоткрытый диапазон; первый индекс включен, а второй исключен. В следующем примере показано воздействие, а на рис. 5 показан результат в памяти:

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

А что произойдет, если мы добавим элемент к s2? Изменяет ли следующий код также s1?

Общий резервный массив изменяется, но изменяется только длина s2. На рис. 7 показан результат добавления элемента к s2.

s1 остается слайсом трех длин и шести емкостей. Следовательно, если мы напечатаем s1 и s2, добавленный элемент будет виден только для s2:

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

ПРИМЕЧАНИЕ.В этих примерах резервный массив является внутренним и недоступен непосредственно разработчику Go. Единственным исключением является случай, когда срез создается путем среза существующего массива.

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

Этот код приводит к созданию еще одного резервного массива. На рисунке 8 показаны результаты в памяти.

s1 и s2 теперь ссылаются на два разных массива. Поскольку s1 по-прежнему представляет собой срез с тремя длинами и шестью емкостью, у него все еще есть некоторый доступный буфер, поэтому он продолжает ссылаться на исходный массив. Кроме того, новый резервный массив был создан путем копирования исходного из первого индекса s2. Вот почему новый массив начинается с элемента 1, а не с 0.

Заключение

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

Этот пост взят из моей книги 100 ошибок в Go и как их избежать, которая была выпущена в августе 2022 года (ошибка #22):

Книга «100 ошибок Go и как их избежать»показывает, как заменить распространенные проблемы программирования в Go идиоматичным, выразительным кодом. В нем вы изучите десятки интересных примеров и тематических исследований и научитесь выявлять ошибки, которые могут появиться в ваших собственных приложениях.

Сэкономьте 35 % с кодом au35har.

Между тем, вот репозиторий GitHub, в котором собраны все ошибки в книге: https://github.com/teivah/100-go-mistakes.