Внутреннее устройство среза и рост ёмкости

1. Три компонента среза

Срез состоит из трёх частей:

┌─────────────┐
│   Pointer   │ → адрес первого элемента базового массива
│   Length    │ → количество элементов (len)
│   Capacity  │ → размер базового массива (cap)
└─────────────┘

Пример

numbers := []int{10, 20, 30, 40, 50}
fmt.Printf("len: %d\n", len(numbers))  // 5
fmt.Printf("cap: %d\n", cap(numbers))  // 5

2. Функции len() и cap()

Функция Описание
len(slice) Количество элементов в срезе
cap(slice) Вместимость — максимум элементов без перевыделения
s := []int{1, 2, 3}
fmt.Println(len(s))  // 3
fmt.Println(cap(s))  // 3

3. Что происходит при append()

Механизм роста ёмкости

Когда len == cap и нужно добавить элемент:

  1. Выделяется новый массив с большей ёмкостью
  2. Копируются все старые элементы
  3. Добавляется новый элемент
  4. Резервируется запас для будущих добавлений
s := []int{1, 2, 3}  // len=3, cap=3
s = append(s, 4)      // len=4, cap=6 (выделено с запасом!)

Цель: Избежать перевыделения при каждом append.


4. Формула роста ёмкости

Общее правило (эмпирическое)

  • Малые срезы (cap < ~256): рост в 2 раза
  • Большие срезы: рост ~25%

Примечание: Точная формула зависит от версии Go и размера элементов.


5. Экспериментальная демонстрация

Код для исследования

slice := []int{}
prevCap := cap(slice)
fmt.Printf("Старт: len=%d, cap=%d\n", len(slice), prevCap)

for i := 0; i < 5000; i++ {
    slice = append(slice, i)
    currentCap := cap(slice)
    
    if currentCap != prevCap {
        delta := currentCap - prevCap
        percent := float64(delta) / float64(prevCap) * 100.0
        
        fmt.Printf("Итерация %d: cap=%d → %d | +%d (%.2f%%)\n",
            i, prevCap, currentCap, delta, percent)
        
        prevCap = currentCap
    }
}

6. Результаты эксперимента

Фаза 1: Удвоение (малые срезы)

Итерация 0:   cap=0 → 4      | +4    (+Inf%)
Итерация 4:   cap=4 → 8      | +4    (100%)
Итерация 8:   cap=8 → 16     | +8    (100%)
Итерация 16:  cap=16 → 32    | +16   (100%)
Итерация 32:  cap=32 → 64    | +32   (100%)
Итерация 64:  cap=64 → 128   | +64   (100%)
Итерация 128: cap=128 → 256  | +128  (100%)
Итерация 256: cap=256 → 512  | +256  (100%)

Паттерн: Удвоение (×2) на малых размерах.


Фаза 2: Переход к пропорциональному росту

Итерация 512:  cap=512 → 848    | +336  (65.62%)
Итерация 848:  cap=848 → 1280   | +432  (50.94%)
Итерация 1280: cap=1280 → 1792  | +512  (40.00%)
Итерация 1792: cap=1792 → 2560  | +768  (42.86%)
Итерация 2560: cap=2560 → 3408  | +848  (33.12%)

Паттерн: Рост замедляется, стремится к ~25-40%.


Фаза 3: Стабилизация (~25%)

Итерация 27648: cap=27648 → 34816  | +7168  (25.93%)
Итерация 34816: cap=34816 → 44032  | +9216  (26.47%)
Итерация 44032: cap=44032 → 55296  | +11264 (25.58%)
Итерация 55296: cap=55296 → 69632  | +14336 (25.93%)

Паттерн: Стабильный рост ~25% на больших размерах.


7. Визуализация процесса

Срез: [1, 2, 3]  len=3, cap=3
        ↓
append(slice, 4)
        ↓
Новый массив: [1, 2, 3, 4, _, _]  len=4, cap=6
              └─ копия ─┘  └─ резерв ─┘

Старый массив остаётся в памяти до garbage collection.


8. Практический вывод

Оптимизация: make() с ёмкостью

Если известно примерное количество элементов:

// ❌ Плохо: много перевыделений
s := []int{}
for i := 0; i < 10000; i++ {
    s = append(s, i)
}

// ✅ Хорошо: одно выделение
s := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
    s = append(s, i)
}

Экономия: Меньше выделений памяти → быстрее.


9. Важные замечания

Непредсказуемость формулы

  • Точная формула роста не гарантирована спецификацией
  • Может меняться между версиями Go
  • Зависит от размера элементов и архитектуры

Общие наблюдения

✅ Малые срезы → рост ×2
✅ Большие срезы → рост ~25%
✅ Цель — баланс между памятью и производительностью

Не полагайтесь на точные значения — используйте len() и cap() для проверки.


10. Итоги

✅ Срез = (pointer, len, cap)
len() — количество элементов
cap() — вместимость до перевыделения
append() автоматически увеличивает cap с запасом
✅ Формула роста: ×2~25% по мере увеличения
✅ Оптимизация: make([]T, 0, capacity) при известном размере

Ключевое правило:

len ≤ cap всегда
append при len=cap → новый массив с cap > старый cap