Пустой срез vs nil-срез и удаление элементов
1. nil-срез vs пустой срез
Два типа “пустых” срезов
var nilSlice []int // nil-срез
emptySlice := []int{} // пустой срез (не nil)
| Характеристика | var s []int |
s := []int{} |
|---|---|---|
| Равен nil | ✅ true |
❌ false |
| len | 0 |
0 |
| cap | 0 |
0 |
| Память выделена | ❌ Нет | ✅ Да (структура среза) |
| Работает с append | ✅ Да | ✅ Да |
Демонстрация различий
var nilSlice []int
emptySlice := []int{}
fmt.Printf("nilSlice == nil: %t\n", nilSlice == nil) // true
fmt.Printf("emptySlice == nil: %t\n", emptySlice == nil) // false
fmt.Printf("len(nilSlice): %d\n", len(nilSlice)) // 0
fmt.Printf("len(emptySlice): %d\n", len(emptySlice)) // 0
fmt.Printf("cap(nilSlice): %d\n", cap(nilSlice)) // 0
fmt.Printf("cap(emptySlice): %d\n", cap(emptySlice)) // 0
2. Семантическая разница
nil-срез
Означает: Память вообще не выделена.
var s []int // s не инициализирован
Визуализация:
s → nil (указатель не установлен)
Пустой срез
Означает: Срез инициализирован, но элементов нет.
s := []int{} // s инициализирован, но пуст
Визуализация:
s → (ptr, len=0, cap=0) → []
3. Оба работают одинаково с append
var nilSlice []int
emptySlice := []int{}
nilSlice = append(nilSlice, 1)
emptySlice = append(emptySlice, 1)
fmt.Println(nilSlice) // [1]
fmt.Println(emptySlice) // [1]
Вывод: Для append() оба среза эквивалентны.
4. Когда использовать какой
| Случай | Используйте |
|---|---|
| Возврат из функции (нет данных) | var s []int (nil) |
| Явная инициализация “пустой коллекцией” | s := []int{} |
| JSON сериализация | []int{} → [], nil → null |
5. Удаление элементов: общий случай
Проблема
В Go нет встроенной функции remove().
Решение через append() и slicing
s := []int{10, 20, 30, 40, 50}
index := 2 // Удаляем элемент с индексом 2 (значение 30)
s = append(s[:index], s[index+1:]...)
fmt.Println(s) // [10 20 40 50]
Механизм:
s[:index] → [10 20] (всё до индекса)
s[index+1:] → [40 50] (всё после индекса)
append(...) → [10 20 40 50] (склеиваем)
Параметризация
func removeAt(s []int, index int) []int {
return append(s[:index], s[index+1:]...)
}
nums := []int{10, 20, 30, 40, 50}
nums = removeAt(nums, 2)
fmt.Println(nums) // [10 20 40 50]
Важно: Функция возвращает новый срез — присваивайте результат.
6. Удаление первого элемента
Простой способ: slicing
nums := []int{1, 2, 3, 4, 5}
fmt.Println(nums) // [1 2 3 4 5]
nums = nums[1:] // Берём всё, начиная с индекса 1
fmt.Println(nums) // [2 3 4 5]
Синтаксис:
s = s[1:] // Удаляет первый элемент
7. Удаление последнего элемента
Простой способ: slicing до len-1
nums := []int{1, 2, 3, 4, 5}
fmt.Println(nums) // [1 2 3 4 5]
nums = nums[:len(nums)-1] // Берём всё до последнего
fmt.Println(nums) // [1 2 3 4]
Синтаксис:
s = s[:len(s)-1] // Удаляет последний элемент
8. Сводка операций удаления
| Операция | Код |
|---|---|
Удалить по индексу i |
s = append(s[:i], s[i+1:]...) |
| Удалить первый | s = s[1:] |
| Удалить последний | s = s[:len(s)-1] |
Удалить первые n |
s = s[n:] |
Удалить последние n |
s = s[:len(s)-n] |
9. Особенности удаления через append()
Модификация базового массива
original := []int{1, 2, 3, 4, 5}
modified := append(original[:2], original[3:]...)
fmt.Println(modified) // [1 2 4 5]
fmt.Println(original) // [1 2 4 5 5] — оригинал тоже изменён!
Причина: Оба среза указывают на один базовый массив.
Безопасное удаление (с копированием)
original := []int{1, 2, 3, 4, 5}
// Создаём копию
safeCopy := make([]int, len(original))
copy(safeCopy, original)
// Удаляем из копии
safeCopy = append(safeCopy[:2], safeCopy[3:]...)
fmt.Println(safeCopy) // [1 2 4 5]
fmt.Println(original) // [1 2 3 4 5] — не изменён
10. Проверка на nil перед операциями
var s []int
if s == nil {
fmt.Println("Срез nil, инициализируем")
s = []int{}
}
s = append(s, 1, 2, 3)
fmt.Println(s) // [1 2 3]
Примечание: Для append() проверка не обязательна — работает с nil-срезами.
11. Почему срезы, а не массивы
Из материала:
“В большинстве случаев использовать мы будем именно срезы, а не классические массивы”.
Причины
| Срезы | Массивы |
|---|---|
| ✅ Динамический размер | ❌ Фиксированная длина |
✅ append() для добавления |
❌ Нельзя изменить размер |
| ✅ Гибкие операции (slicing) | ❌ Копируются целиком |
| ✅ Передача по ссылке | ❌ Передача по значению |
Вывод: Срезы — основная коллекция в Go.
12. Итоги
✅ nil-срез: память не выделена, s == nil истинно
✅ Пустой срез: инициализирован, но без элементов (len=0)
✅ Оба работают с append() одинаково
✅ Удаление: append(s[:i], s[i+1:]...)
✅ Первый элемент: s = s[1:]
✅ Последний элемент: s = s[:len(s)-1]
✅ Всегда присваивайте результат функций append() и slicing
✅ Срезы — предпочтительнее массивов в Go
Ключевое правило:
var s []int → nil-срез (память не выделена)
s := []int{} → пустой срез (инициализирован)
Удаление по индексу:
s = append(s[:i], s[i+1:]...)
Тест: Ссылочная природа срезов и операции с ними
Вопрос 1: Поверхностное копирование vs глубокое копирование
Что произойдёт при выполнении следующего кода?
original := []int{1, 2, 3}
copySlice := original
original[0] = 999
fmt.Println(copySlice[0])
Варианты ответов:
a) Выведет 1 — copySlice остался без изменений
b) ✅ Выведет 999 — оба среза указывают на один базовый массив
c) Произойдёт ошибка компиляции
d) Выведет 0 — copySlice обнулился
Правильный ответ: b
Объяснение: При присваивании copySlice := original копируется только структура среза (указатель, длина, ёмкость), но не данные. Оба среза указывают на один и тот же базовый массив в памяти, поэтому изменения через original видны и через copySlice. soumendrak
Вопрос 2: Функция copy() и её поведение
Что верно относительно функции copy(dst, src) в Go? (Может быть несколько правильных ответов)
a) ✅ Создаёт глубокую копию данных — изменения в dst не влияют на src
b) ✅ Копирует min(len(dst), len(src)) элементов
c) Автоматически увеличивает размер dst, если src больше
d) ✅ Возвращает количество скопированных элементов
Правильные ответы: a, b, d
Объяснение: Функция copy() создаёт независимую копию данных в новый массив. Она копирует столько элементов, сколько помещается в приёмник (min(len(dst), len(src))), и возвращает это количество. Функция не изменяет размер dst автоматически — нужно заранее выделить достаточный размер через make(). w3schools
Вопрос 3: nil-срез vs пустой срез
Чем отличаются var s1 []int и s2 := []int{}? (Может быть несколько правильных ответов)
a) ✅ s1 == nil вернёт true, а s2 == nil вернёт false
b) ✅ s1 не имеет выделенной памяти, s2 инициализирован (указатель на пустой массив)
c) s1 нельзя использовать с append(), а s2 можно
d) ✅ Оба имеют len=0 и cap=0, но семантически различны
Правильные ответы: a, b, d
Объяснение: nil-срез (var s []int) не имеет выделенной памяти и равен nil, тогда как пустой срез ([]int{}) инициализирован структурой с указателем на пустой массив. Оба имеют нулевую длину и ёмкость, и оба корректно работают с append(). Различие важно для JSON-сериализации: nil → null, пустой срез → []. victoriametrics
Эти тесты охватывают критические концепции: ссылочную природу срезов при присваивании, механизм глубокого копирования через copy(), и семантическое различие между nil-срезом и пустым срезом.