← Назад к вопросам

Насколько увеличивается слайс при превышении capacity

1.8 Middle🔥 61 комментариев
#Основы Go

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Отличный вопрос, который напрямую касается понимания внутренней механики slices в Go. Это классическая тема на собеседованиях.

Краткий ответ

При добавлении элемента в слайс, чья длина (len) достигла его текущей ёмкости (cap), происходит аллокация нового, большего внутреннего массива. Старые данные копируются в него, и слайс начинает ссылаться на эту новую область памяти. Размер нового массива (а значит, и новая ёмкость слайса) определяется стратегией роста, реализованной в runtime Go. Начиная с версии Go 1.18, эта стратегия стала более плавной.

Детальный механизм роста

Процесс можно разбить на этапы:

  1. Проверка. При вызове append(slice, element) проверяется, достаточно ли ёмкости (cap(slice) > len(slice)). Если да — новый элемент помещается в "хвост" существующего массива, и длина увеличивается на 1.
  2. Нехватка ёмкости. Если места нет (cap(slice) == len(slice)), runtime инициирует процесс переаллокации.
  3. Выделение нового массива. Вычисляется новая ёмкость по определённым правилам.
  4. Копирование. Все элементы из старого массива копируются в новый.
  5. Добавление. Новый элемент добавляется в конец нового массива.
  6. Возврат. Функция append возвращает новый слайс, который теперь ссылается на новый массив. Старый слайс (если он не был переприсвоен) продолжает ссылаться на старый массив.

Алгоритм вычисления новой capacity (с Go 1.18+)

До Go 1.18 слайс увеличивался вдвое при ёмкости меньше 1024 элементов, а после — на 25%. Сейчас алгоритм более сложный и плавный. Он стремится сгладить скачкообразное изменение множителя роста.

package main

import "fmt"

func main() {
    s := []int{} // len=0, cap=0
    var lastCap int

    for i := 0; i <卡 2000; i++ {
        s = append(s, i)
        if cap(s) != lastCap {
            fmt.Printf("len: %d, new cap: %d, growth factor: %.2f\n",
                len(s), cap(s), float64(cap(s))/float64(lastCap))
            lastCap = cap(s)
        }
    }
}

Пример вывода (первые несколько шагов):

len: 1, new cap: 1, growth factor: +Inf (от 0 до 1)
len: 2, new cap: 2, growth factor: 2.00
len: 3, new cap: 4, growth factor: 2.00
len: 5, new cap: 8, growth factor: 2.00
len: 9, new cap: 16, growth factor: 2.00
...
len: 1025, new cap: 1280, growth factor: 1.25
len: 1281, new cap: 1696, growth factor: ~1.32

Как видно, множитель постепенно уменьшается от 2.0 к примерно 1.25 по мере увеличения слайса.

Ключевые правила (упрощённо):

  • Для маленьких слайсов рост всё ещё близок к удвоению.
  • Для очень больших слайсов (cap > 4КБ или больше) множитель асимптотически стремится к 1.25, чтобы минимизировать избыточную память (overhead).
  • Алгоритм учитывает размер элемента. Удваивается не количество элементов, а общий размер памяти, что затем пересчитывается в количество элементов с учётом выравнивания (alignment).

Практические последствия и советы

  • Производительность. Переаллокация и копирование — дорогие операции. Если конечный размер слайса известен заранее или можно его оценить, всегда используйте предварительную аллокацию через make([]T, 0, estimatedCapacity). Это полностью избегает промежуточных переаллокаций.

    // ПЛОХО: возможны множественные переаллокации.
    var data []string
    for i := 0; i < 10000; i++ {
        data = append(data, getData(i))
    }
    
    // ОТЛИЧНО: одна аллокация.
    data := make([]string, 0, 10000)
    for i := 0; i < 10000; i++ {
        data = append(data, getData(i))
    }
    
  • "Утечки" памяти. Старый внутренний массив может остаться в памяти, если на него есть другие ссылки (например, через "срез" — slice). Это важно при работе с большими данными.

    func processBigSlice() {
        bigData := make([]byte, 0, 10_000_000)
        bigData = append(bigData, 1)
        // Допустим, мы работали с bigData...
    
        smallSlice := bigData[:1] // ВАЖНО! smallSlice всё ещё ссылается на огромный массив в 10МБ!
        // bigData = nil // Чтобы освободить большой массив, нужно явно потерять на него ссылку.
        use(smallSlice)
    }
    
  • Предсказуемость append. Всегда присваивайте результат append обратно той же переменной или используйте его непосредственно: s = append(s, elem).

Заключение

Слайс увеличивается не по простой формуле "вдвое до 1024", а по более сложному алгоритму, который с Go 1.18 оптимизирует использование памяти для слайсов любого размера. Понимание этого процесса критически важно для написания эффективного кода, так как позволяет избежать неожиданных падений производительности из-за частых переаллокаций. Золотое правило: предварительно аллоцируйте слайс с помощью make, если вам известен или может быть приблизительно оценён его конечный размер.

Насколько увеличивается слайс при превышении capacity | PrepBro