Внутреннее устройство среза и рост ёмкости
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 и нужно добавить элемент:
- Выделяется новый массив с большей ёмкостью
- Копируются все старые элементы
- Добавляется новый элемент
- Резервируется запас для будущих добавлений
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