Как работает `append` в Go по отношению к `len` и `cap` слайса? Что происходит при исчерпании `capacity`?

Ответ

Функция append добавляет элементы в конец слайса. Её поведение напрямую зависит от соотношения его длины (len) и ёмкости (cap).

Есть два основных сценария:

  1. Ёмкость не превышена (len < cap):

    • Новый элемент добавляется в существующий базовый массив, на который ссылается слайс.
    • len слайса увеличивается на 1.
    • Новый массив не создается. Изменения происходят "на месте".
  2. Ёмкость исчерпана (len == cap):

    • Go выделяет новый, больший массив.
    • Все элементы из старого массива копируются в новый.
    • Новый элемент добавляется в конец нового массива.
    • Функция append возвращает новый слайс, который ссылается на этот новый массив. len и cap этого нового слайса обновляются.

Стратегия роста capacity:

  • Для слайсов до 1024 элементов ёмкость обычно удваивается (cap * 2).
  • Для больших слайсов ёмкость растет медленнее, примерно на 25% (cap * 1.25), чтобы избежать чрезмерного расхода памяти.

Ключевые правила при работе с append:

  • Всегда присваивайте результат: Так как append может вернуть новый слайс, всегда используйте конструкцию slice = append(slice, elem). Иначе вы рискуете потерять данные при переаллокации.
  • Остерегайтесь сайд-эффектов: Если вы делаете append в срез, созданный из другого слайса, и capacity не превышена, вы можете неожиданно изменить данные в исходном слайсе.

Пример сайд-эффекта:

original := []int{1, 2, 3, 4, 5} // len=5, cap=5
subSlice := original[1:3]       // {2, 3}, len=2, cap=4 (делит массив с original)

fmt.Printf("Original до: %vn", original)

// Append к subSlice не вызовет переаллокации, т.к. cap=4
subSlice = append(subSlice, 99)

// Изменился и исходный слайс!
fmt.Printf("Original после: %vn", original) // Вывод: Original после: [1 2 3 99 5]
fmt.Printf("Sub-slice: %vn", subSlice)     // Вывод: Sub-slice: [2 3 99]