Перейти к содержимому

Практика 6. `append`, `copy`, `len`, `cap` и общее хранилище slices

Цель

Понять внутреннюю модель slice: длину, вместимость, общее базовое хранилище и возможное перевыделение при append. Научиться создавать независимые копии, вставлять и удалять элементы, сортировать данные без изменения оригинала.

Общие требования

  • создайте модуль example.com/practice-06;
  • каждый эксперимент сопровождайте фактическим выводом len и cap;
  • не полагайтесь на конкретный коэффициент роста вместимости;
  • операции, обещающие сохранить исходные данные, должны действительно создавать независимый результат;
  • выполните go fmt ./..., go test ./..., go vet ./....

A. Эксперименты

Задание 1. Рост slice

Создайте:

values := make([]int, 0, 2)

Добавьте числа от 1 до 12. После каждого append выведите:

value=1 len=1 cap=2 values=[1]

Ответьте в README.md:

  1. В какие моменты менялась вместимость?
  2. Совпадает ли она всегда с длиной?
  3. Можно ли рассчитывать на конкретную последовательность значений cap?

Задание 2. Длина не равна вместимости

Создайте:

values := make([]int, 3, 8)

Заполните существующие три элемента через индексы, затем добавьте ещё два через append. После каждого шага выведите данные, длину и вместимость.

Объясните, почему сразу после make доступны индексы 0–2, но индекс 3 ещё недоступен, несмотря на cap == 8.

Задание 3. Общее хранилище

Запустите и объясните:

numbers := []int{10, 20, 30, 40, 50}
left := numbers[:3]
right := numbers[2:]
left[2] = 999
fmt.Println("numbers:", numbers)
fmt.Println("left:", left)
fmt.Println("right:", right)

Нарисуйте в README.md простую схему: один базовый массив и два диапазона, которые на него указывают.

Задание 4. append к подсрезу

Исследуйте:

numbers := []int{10, 20, 30, 40}
part := numbers[:2]
part = append(part, 777)

Выведите numbers и part. Затем повторите с ограничением вместимости:

numbers := []int{10, 20, 30, 40}
part := numbers[:2:2]
part = append(part, 777)

Объясните различие.


B. Основные задания

Задание 5. Независимая копия

Реализуйте два варианта:

func cloneWithCopy(values []int) []int
func cloneWithSlices(values []int) []int

Первый использует make и copy, второй — slices.Clone.

Проверка должна:

  1. клонировать исходные данные;
  2. изменить первый элемент клона;
  3. доказать, что оригинал не изменился;
  4. корректно обработать nil и пустой slice.

Задание 6. Удаление

Реализуйте:

func deleteAt(values []int, index int) ([]int, bool)

Требования:

  • некорректный индекс возвращает исходный slice и false;
  • корректный индекс удаляется с сохранением порядка;
  • функция возвращает новый slice;
  • исходный slice и его базовое хранилище не должны изменяться.

Подсказка: сначала клонируйте данные, затем используйте slices.Delete.

Дополнительно реализуйте быстрый вариант без сохранения порядка и объясните, в каких задачах он допустим.

Задание 7. Вставка

func insertAt(values []int, index int, value int) ([]int, bool)

Разрешите индексы от 0 до len(values) включительно. Индекс len(values) означает добавление в конец. Исходный slice не изменяется.

Используйте slices.Insert после проверки границы и клонирования.

Задание 8. Сортировка без изменения оригинала

func sortedCopy(values []int) []int

Функция должна:

  • вернуть независимую копию;
  • отсортировать её по возрастанию;
  • оставить исходный порядок без изменений.

Дополнительно создайте вариант по убыванию через slices.SortFunc.

Задание 9. Уникальные соседние элементы

Дан отсортированный slice:

values := []int{1, 1, 2, 2, 2, 4, 5, 5}

Получите [1 2 4 5] через slices.Compact. Не изменяйте оригинал: сначала создайте копию.

Объясните, почему slices.Compact удаляет только соседние повторы и зачем для произвольного набора может понадобиться сортировка или map.

Задание 10. Фильтрация с повторным использованием памяти

Реализуйте:

func positiveCopy(values []int) []int
func positiveInPlace(values []int) []int

Первый вариант создаёт независимый результат. Второй использует:

result := values[:0]

Покажите, как второй вариант изменяет базовое хранилище исходного slice. В README.md опишите, когда такой вариант допустим и когда опасен.


C. Проект «Редактор списка задач»

Продолжите проект из практики 5. Пока данные хранятся в двух согласованных slices:

titles := []string{
"Подготовить отчёт",
"Проверить почту",
"Исправить ошибку",
}
priorities := []int{5, 2, 4}

Индекс названия должен соответствовать индексу приоритета. Реализуйте:

func validateStorage(titles []string, priorities []int) bool
func addTask(
titles []string,
priorities []int,
title string,
priority int,
) (newTitles []string, newPriorities []int, ok bool)
func deleteTask(
titles []string,
priorities []int,
index int,
) (newTitles []string, newPriorities []int, ok bool)
func insertTask(
titles []string,
priorities []int,
index int,
title string,
priority int,
) (newTitles []string, newPriorities []int, ok bool)

Требования:

  • функции проверяют одинаковую длину двух slices;
  • приоритет находится в диапазоне 1–5;
  • название после strings.TrimSpace не пустое;
  • индекс проверяется до обращения к элементу;
  • операции возвращают независимые slices;
  • при ошибке исходные данные не меняются;
  • вызывающая сторона сохраняет оба возвращённых slice;
  • после каждой операции вызывается validateStorage.

Сценарий проверки:

  1. добавьте задачу в конец;
  2. вставьте задачу в начало;
  3. вставьте задачу в середину;
  4. удалите последнюю задачу;
  5. попробуйте удалить по отрицательному индексу;
  6. попробуйте добавить приоритет 8;
  7. докажите, что исходный snapshot не изменился.

Для snapshot используйте:

originalTitles := slices.Clone(titles)
originalPriorities := slices.Clone(priorities)

Аналитический вопрос

Почему два согласованных slice неудобны и опасны? Как структура Task, которая будет изучена в следующей лекции, устранит эту проблему?

Дополнительные задания

Ограничение удерживаемой памяти

Создайте slice на 10 миллионов элементов и оставьте первые 10. Сравните два варианта:

small := large[:10]

и:

small := slices.Clone(large[:10])

Объясните разницу без попытки вручную управлять сборщиком мусора.

Очередь

Реализуйте простую FIFO-очередь на slice с функциями enqueue и dequeue. dequeue возвращает значение, новый slice и bool. Обработайте пустую очередь.

Критерии оценивания

КритерийБаллы
Эксперименты с len, cap и append0–3
Понимание общего хранилища0–3
Копирование, вставка и удаление0–3
Сортировка и фильтрация0–3
Проект «Редактор списка задач»0–4
Максимум16

Вопросы для защиты

  1. Что хранит значение slice?
  2. Почему доступ ограничен len, а не cap?
  3. Когда append может изменить исходный массив?
  4. Что означает запись values[:2:2]?
  5. Как создать независимую копию slice?
  6. Почему slices.Sort может изменить данные вызывающей стороны?
  7. Чем фильтрация на месте отличается от создания нового результата?
  8. Почему две связанные последовательности лучше заменить slice структур?