Что такое слайс
Слайс (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 из нескольких горутин — гонка
данных. Синхронизируйте через мьютекс или канал, либо давайте каждой горутине
свой слайс и сливайте результат.
Частые ошибки
- Забыть присвоить результат
append. appendв подслайс, затирающий общий массив.copyв неинициализированный приёмник.- Сравнивать слайсы через
==(нельзя; только сnil). - Подслайс, удерживающий гигантский базовый массив.
На собеседовании спросят
- Из чего состоит слайс? (ptr, len, cap)
- Чем отличается nil-слайс от пустого? Как это видно в JSON?
- Что выведет код с
appendв подслайс? (вопрос на общий базовый массив) - Зачем нужен третий индекс в
s[a:b:c]? - Как растёт ёмкость и почему не на единицу?
- Как из-за слайса утекает память и как это починить?
- Почему
appendобязательно присваивать обратно?