Слайсы в Go: устройство, append и подводные камни на собеседовании

Обновлено 2026-05-12

Что такое слайс

Слайс (slice) — это гибкая, динамически растущая «вьюшка» поверх массива фиксированной длины. Сам массив в Go редко используют напрямую: его длина зашита в тип ([3]int и [4]int — разные типы), и при передаче он копируется целиком. Слайс снимает оба ограничения — поэтому почти весь код на Go работает именно со слайсами.

s := make([]int, 0, 4) // длина 0, ёмкость 4
s = append(s, 1, 2, 3)
fmt.Println(s, len(s), cap(s)) // [1 2 3] 3 4

Массив против слайса

МассивСлайс
Длиначасть типа, фиксированнаяменяется через append
Передачакопируется целикомкопируется дескриптор, данные общие
Тип[N]T[]T

Внутреннее устройство: дескриптор

Слайс — это маленькая структура из трёх полей (slice header):

type slice struct {
    ptr *T  // указатель на начало данных в базовом массиве
    len int // сколько элементов доступно
    cap int // сколько помещается до конца базового массива
}

Когда вы передаёте слайс в функцию, копируется именно этот дескриптор (24 байта на 64-битной платформе), а не данные. Поэтому изменение элементов видно снаружи, а переназначение длины через append — не всегда (об этом ниже).

Создание слайсов

var a []int            // nil-слайс: ptr=nil, len=0, cap=0
b := []int{}           // пустой, но НЕ nil
c := []int{1, 2, 3}    // литерал, len=cap=3
d := make([]int, 2)    // [0 0], len=cap=2
e := make([]int, 0, 8) // len=0, cap=8 — заранее выделили память

len и cap

len — сколько элементов уже есть. cap — сколько влезет без перевыделения, считая от начала слайса до конца базового массива. Пока len < cap, append пишет в уже выделенную память и не аллоцирует.

Механика append

append возвращает (возможно, новый) слайс — всегда присваивайте результат:

s = append(s, x) // правильно
append(s, x)     // ошибка: результат потерян

Если len < cap, элемент дописывается на месте. Если места нет, рантайм выделяет новый, больший массив, копирует туда старые данные и возвращает слайс с новым ptr. Старый базовый массив живёт, пока на него кто-то ссылается.

Рост ёмкости

Когда ёмкости не хватает, она увеличивается не на единицу, а кратно — чтобы амортизировать стоимость роста. Грубое правило: на маленьких размерах ёмкость примерно удваивается, на больших растёт медленнее (≈ в 1.25 раза). Точные пороги и формула роста менялись между версиями Go (раньше удвоение шло примерно до 1024 элементов, в новых версиях формула мягче), поэтому полагаться на конкретные числа в коде нельзя. Если знаете итоговый размер — задайте cap сразу через make.

Общий базовый массив — главный подвох

Нарезка не копирует данные: подслайс смотрит в тот же массив.

a := []int{1, 2, 3, 4, 5}
b := a[1:3]      // [2 3], но cap(b)=4 (до конца a)
b[0] = 99        // меняет и a: a == [1 99 3 4 5]
b = append(b, 7) // влезает в ёмкость → перезаписывает a[3]!
// a == [1 99 3 7 5]

Классический баг: append в подслайс молча портит «соседей», пока есть свободная ёмкость.

Полное выражение среза: s[low:high
]

Третий индекс ограничивает ёмкость (cap = max - low). Это защищает базовый массив: следующий append гарантированно выделит новый массив, а не затрёт чужие данные.

b := a[1:3:3]    // len=2, cap=2
b = append(b, 7) // ёмкости нет → новый массив, a не тронут

copy — честное копирование

dst := make([]int, len(src))
n := copy(dst, src) // копирует min(len(dst), len(src)) элементов

Частая ошибка — copy в nil/пустой приёмник: скопируется 0 элементов. Приёмник надо заранее выделить нужной длины.

nil против пустого слайса

Оба имеют len == 0, но это не одно и то же:

var a []int     // nil:  a == nil → true
b := []int{}    // пустой: b == nil → false

Для проверки «пусто ли» используйте len(s) == 0 — работает для обоих. Разница видна в JSON: nil-слайс маршалится в null, пустой ненулевой слайс — в [].

Удаление элементов

С сохранением порядка (для обычных значений):

i := 2
s = append(s[:i], s[i+1:]...)

Если в слайсе указатели или ссылки, после сдвига в «хвосте» базового массива остаётся висячая ссылка — её надо обнулить, иначе объект не соберёт GC. Безопасный идиоматичный вариант:

copy(s[i:], s[i+1:]) // сдвигаем хвост влево
s[len(s)-1] = nil    // обнуляем дубликат в конце
s = s[:len(s)-1]     // отрезаем последний элемент

Утечки памяти

Подслайс держит весь базовый массив, даже если смотрит на пару элементов:

func first2(huge []byte) []byte {
    return huge[:2] // удерживает весь huge в памяти!
}

Лечится копированием нужного куска в свежий слайс (append([]byte(nil), huge[:2]...)), чтобы большой массив смог собрать GC.

Передача в функции

Копируется дескриптор. Поэтому изменения элементов видны снаружи, а append внутри функции виден снаружи только если не вызвал перевыделение и вы вернули результат. Надёжно — возвращать слайс из функции.

Конкурентность

Слайс не потокобезопасен. Параллельные append из нескольких горутин — гонка данных. Синхронизируйте через мьютекс или канал, либо давайте каждой горутине свой слайс и сливайте результат.

Частые ошибки

На собеседовании спросят