Как работает механизм расширения слайса в Go?

Ответ

Слайс в Go — это легковесная структура-дескриптор, которая содержит три поля: указатель на начало нижележащего массива, длину (len) и ёмкость (cap).

Расширение происходит при использовании встроенной функции append. Алгоритм следующий:


  1. Проверка ёмкости: Если текущая ёмкость (cap) достаточна для добавления новых элементов, они просто помещаются в существующий массив, а длина (len) слайса увеличивается. Указатель на массив не меняется.


  2. Аллокация нового массива: Если ёмкости недостаточно, рантайм Go выделяет новый, больший массив. Затем происходит:

    • Копирование всех элементов из старого массива в новый.
    • Добавление новых элементов в конец.
    • Слайс обновляется: его указатель теперь ссылается на новый массив, а len и cap обновляются.

Стратегия роста ёмкости:
Алгоритм роста не удваивает ёмкость всегда. До Go 1.18, для слайсов до 1024 элементов ёмкость удваивалась, а после — росла на ~25%. Начиная с Go 1.18, алгоритм стал более плавным, но общая идея сохранилась: рост происходит с запасом, чтобы амортизировать стоимость аллокаций.

// Создаем слайс с len=0 и cap=3
s := make([]int, 0, 3)
fmt.Printf("len=%d, cap=%d, ptr=%pn", len(s), cap(s), s)

// Добавляем 3 элемента. Ёмкости хватает.
s = append(s, 1, 2, 3)
fmt.Printf("len=%d, cap=%d, ptr=%pn", len(s), cap(s), s) // ptr тот же

// Добавляем еще один элемент. Ёмкости не хватает.
s = append(s, 4)
// Происходит аллокация нового массива (cap станет 6) и копирование.
fmt.Printf("len=%d, cap=%d, ptr=%pn", len(s), cap(s), s) // ptr изменился

Важный нюанс: Если слайс был создан на основе другого массива или слайса (sliceB := sliceA[:5]), append в sliceB может перезаписать данные в sliceA, если ёмкости хватает. Если не хватает — будет создан новый массив, и связь разорвется. Это частый источник ошибок.