Slice: len, cap, append и реаллокация

Зачем это на интервью

Slice — основной контейнер Go. Важно отличать его маленький заголовок от массива за ним: это объясняет стоимость append, неожиданные изменения данных и причины лишних аллокаций.

Минимум для E4

  • Объяснить, что len — доступная длина, а cap — граница роста в том же массиве.
  • Знать, что append возвращает новый slice и может выделить новый массив.
  • Предвыделять ёмкость, когда известен размер результата.

Углубление для E5/Senior

Заголовок slice содержит указатель на backing array, длину и capacity; его копирование не копирует элементы. Алгоритм роста capacity — деталь runtime, не контракт: нельзя строить корректность или точный memory budget на предположении о коэффициенте роста. Для горячего пути измеряют аллокации (go test -benchmem), а не оптимизируют на глаз.

Ключевые понятия

make([]T, n, c) создаёт длину n и capacity c, где n <= c. Индексировать можно только до len-1; s[:cap(s)] расширяет видимую длину в пределах backing array. append(s, x) записывает в свободную capacity либо выделяет массив, копирует старые элементы и возвращает новый заголовок.

func squares(n int) []int {
	out := make([]int, 0, n) // len=0, cap=n
	for i := 0; i < n; i++ {
		out = append(out, i*i)
	}
	return out
}
 
s := make([]int, 2, 3)
s[0], s[1] = 10, 20
s = append(s, 30) // та же backing storage допустима
s = append(s, 40) // может выделить новую; старый s нельзя ожидать изменённым

Не игнорируйте результат append: функция получает копию заголовка, поэтому изменение длины останется локальным, если не вернуть или не присвоить результат.

Типовые вопросы

  1. Чем len отличается от cap?
    • len определяет доступные элементы, cap — максимум длины при реслайсе без новой аллокации.
  2. Всегда ли append выделяет память?
    • Нет: только когда свободной capacity недостаточно; это нельзя проверять по фиксированному правилу роста.
  3. Почему append надо присваивать?
    • Он возвращает заголовок с новой длиной, а иногда и новым указателем.
  4. Когда использовать make([]T, 0, n)?
    • Когда элементы будут добавляться append и известна ожидаемая верхняя оценка n.
  5. Что произойдёт при make([]int, 10) и затем append?
    • В slice уже десять нулевых элементов; append добавит после них. Для пустого результата нужен make([]int, 0, 10).

Практика

  • Реализуйте Filter([]int, func(int) bool) []int с make(..., 0, len(in)). Готово: порядок сохранён, вход не меняется, benchmark показывает не более одной аллокации для непустого результата.
  • Объясните на примере переход len/cap после трёх append. Готово: ответ не предполагает конкретный коэффициент роста runtime.

Частые ошибки и ловушки

  • Путать make([]T, n) с резервированием места.
  • Сохранять указатель на элемент и рассчитывать на его актуальность после append с реаллокацией.
  • Выводить алгоритм роста capacity из одного запуска и считать его API.

Связанные темы

Коллекции и работа с данными · Аллокации и GC

Источники