Как устроены слайсы в Go и как происходит выделение памяти для них?

Ответ

Слайс (slice) в Go — это не сам массив, а легковесная структура-дескриптор, которая описывает непрерывный сегмент базового массива (underlying array). Эта структура состоит из трех полей:

  1. Указатель (pointer): Адрес первого элемента базового массива, доступного через слайс.
  2. Длина (length): Количество элементов в слайсе. Это то, что возвращает функция len().
  3. Ёмкость (capacity): Максимальное количество элементов, которое можно разместить в базовом массиве, начиная с указателя слайса, без необходимости новой аллокации. Это то, что возвращает функция cap().

Процесс выделения памяти

Выделение памяти происходит в момент создания слайса и зависит от способа его инициализации:

  • Через make(): s := make([]T, length, capacity)

    • В куче (heap) выделяется память под базовый массив размером capacity * sizeof(T).
    • Создается структура-заголовок слайса, в которую записываются указатель на этот массив, length и capacity.
    • Оптимизация: Если компилятор с помощью анализа escape-последовательностей (escape analysis) докажет, что слайс не "сбежит" из функции, где он был создан, память под массив может быть выделена на стеке (stack), что гораздо быстрее.
  • Через литерал: s := []T{1, 2, 3}

    • Происходит то же самое: создается базовый массив (в данном случае размером 3), и слайс указывает на него. length и capacity будут равны 3.
  • При создании слайса из существующего массива или слайса: s2 := s[1:3]

    • Новый массив не создается! Создается только новый заголовок слайса (s2), который указывает на тот же самый базовый массив, что и s, но с другим смещением, длиной и ёмкостью. Это делает операцию очень быстрой.

Рост слайса при append

Когда вы добавляете элементы в слайс с помощью append, и его length превышает capacity, происходит следующее:

  1. Аллокация нового, большего массива. Стратегия роста зависит от текущего размера:
    • Для слайсов до 1024 элементов новая ёмкость обычно удваивается (new_cap = old_cap * 2).
    • Для больших слайсов рост замедляется, и новая ёмкость увеличивается примерно в 1.25 раза (new_cap = old_cap * 1.25). Это предотвращает слишком быстрый расход памяти.
  2. Копирование всех элементов из старого массива в новый.
  3. Обновление указателя в заголовке слайса, чтобы он указывал на новый массив. length и capacity также обновляются.

Из-за этого append может быть дорогой операцией, если вызывает переаллокацию. Поэтому, если вы заранее знаете примерный размер слайса, лучше сразу задавать capacity через make.