Пустой срез 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{}[], nilnull

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-срезом и пустым срезом.