JavaRush /Курсы /Go SELF /Пакеты slices, maps, cmp

Пакеты slices, maps, cmp

Go SELF
27 уровень , 2 лекция
Открыта

1. Зачем брать slices/maps/cmp и где они лежат

Если вы только начинаете, то очень легко попасть в ловушку: «я же умею писать for, зачем мне какие-то пакеты». И правда — цикл for в Go универсален. Но проблема не в том, что мы не можем написать цикл. Проблема в том, что ручной цикл почти всегда прячет намерение.

Когда вы видите slices.Delete(tasks, i, j), мозг читает это как: «удалить диапазон». Когда вы видите tasks = append(tasks[:i], tasks[j:]...), мозг читает это как: «эээ… так… стоп… а тут точно границы правильные?..».

А ещё стандартная библиотека — это набор решений, которые уже прошли путь «тысячи граблей», включая тонкие вещи вроде сохранения типа для именованных слайсов и менее неожиданного поведения для сборщика мусора. Пакет slices прямо задумывался как набор generic-хелперов для слайсов любого типа.

Чтобы не говорить просто так, будем опираться на маленькое учебное приложение: мини-трекер задач в памяти. У нас есть Task, слайс задач и иногда map-индекс для быстрого доступа.

package main

	import "fmt"

type Task struct {
	ID       int
	Title    string
	Priority int
	Done     bool
}

func main() {
	tasks := []Task{
		{ID: 1, Title: "Buy milk", Priority: 2},
		{ID: 2, Title: "Learn Go", Priority: 1},
	}
	fmt.Println(tasks) // [{1 Buy milk 2 false} {2 Learn Go 1 false}]
}

Мини-карта пакетов и связь с generics

Когда видишь slices.SortFunc или maps.Clone, хочется спросить: «а почему это не методы?» или «а почему не в sort?» — и это нормальные вопросы. Ответ простой: эти пакеты появились как удобная надстройка над типовыми операциями, но уже в эпоху generics. В релизе Go 1.21 отмечалось добавление пакетов slices, maps и cmp в стандартную библиотеку.

Очень полезно держать в голове «карту намерений»:

Пакет Про что он Типовое ощущение в коде
slices
операции над []T «Я делаю то же самое, что делал циклом, но короче и читаемее»
maps
операции над map[K]V «Я копирую/сравниваю/достаю ключи без ручного for range»
cmp
сравнение упорядочиваемых значений «Я задаю порядок через готовые сравнения, меньше самописной рутины»

Отдельная «магия», которую важно понимать для чтения чужого кода: в сигнатурах slices и maps вы часто увидите S ~[]E или M ~map[K]V. Это сделано, чтобы функции работали не только с обычными []T, но и с именованными типами-слайсами/мапами (например, type TaskList []Task). Из-за этого сигнатуры иногда выглядят «длиннее, чем хотелось бы», зато API получается более практичным.

2. slices: типовые операции без «акробатики с индексами»

Пакет slices — это, по сути, «набор самых частых „операций руками“ над слайсом, но с читабельной кнопкой». Это особенно ценно в учебных и боевых проектах по одной причине: операции над границами слайса — самый быстрый способ словить баг в стиле «почему оно иногда работает?». И второй бонус: slices выражает намерение, а намерение — это половина поддержки кода.

slices.Clone: копирование как «граница владения»

Иногда мы хотим взять список задач и отсортировать «для вывода», но не менять оригинальный порядок (например, в памяти мы храним как добавляли, а выводим красиво). Вот здесь Clone — прямой сигнал: «я делаю копию и дальше буду портить её как хочу».

package main

import (
	"fmt"
	"slices"
)

func main() {
	tasks := []int{3, 1, 2}
	cp := slices.Clone(tasks)

	cp[0] = 999
	fmt.Println(tasks) // [3 1 2]
	fmt.Println(cp)    // [999 1 2]
}

Сигнатура Clone в стандартной библиотеке сделана так, чтобы сохранять тип для именованных слайсов, а не «ронять» всё обратно в []T. Это как раз тот случай, когда стандартное решение аккуратнее самописного «скопирую через append([]T(nil), s...)».

slices.Sort и slices.SortFunc: сортировка “на месте”

Сортировка — идеальный пример различия «функция меняет» и «функция возвращает». slices.Sort и slices.SortFunc сортируют на месте, то есть меняют порядок элементов существующего слайса и ничего не возвращают. Это удобно: в коде видно, что вы не ожидаете новый слайс.

Представим, что мы хотим отсортировать задачи по ID (просто ради примера). Для встроенных упорядочиваемых типов есть slices.Sort, но для структур нужен SortFunc.

package main

import (
	"fmt"
	"slices"
)

type Task struct {
	ID    int
	Title string
}

func main() {
	tasks := []Task{{ID: 2, Title: "B"}, {ID: 1, Title: "A"}}

	slices.SortFunc(tasks, func(a, b Task) int {
		if a.ID < b.ID {
			return -1
		}
		if a.ID > b.ID {
			return 1
		}
		return 0
	})

	fmt.Println(tasks) // [{1 A} {2 B}]
}

Да, компаратор выглядит чуть «многословно». Но это честная цена за то, что порядок у структур почти всегда доменный (мы сами решаем, что важнее: приоритет, дата, ID, заголовок…).

slices.Delete: удаление диапазона безопаснее, чем append руками

Удаление из середины слайса — классика жанра. До slices.Delete все писали вариации append(s[:i], s[j:]...). Это работает, но выглядит как заклинание: если ошибёшься в одной руне — вызываешь демона off-by-one.

Delete прямо выражает намерение: удалить диапазон [i, j) (как и у обычного среза s[i:j], правая граница не включается).

package main

import (
	"fmt"
	"slices"
)

func main() {
	ids := []int{10, 20, 30, 40}
	ids = slices.Delete(ids, 1, 3) // удаляем 20 и 30
	fmt.Println(ids)               // [10 40]
}

Обратите внимание на важный «механический» момент: Delete возвращает новый слайс (точнее, новую «шапку» слайса), поэтому почти всегда нужно присваивание ids = …. Это прямой родственник append: если меняется длина — возвращаем значение.

3. maps: копирование, сравнение и ключи без ручного range

Пакет maps делает для map[K]V примерно то же, что slices для []T: убирает рутину, где вы каждый раз пишете один и тот же цикл. Это не «обязательный пакет», но он отлично работает как маркер намерения: вы показываете читателю, что делаете копию, сравнение или перенос данных, и не отвлекаете его на механику.

Важно помнить: maps не отменяет ключевое свойство map — порядок итерации всё ещё не гарантируется. Поэтому maps удобно сочетать с slices и cmp, когда нужен стабильный вывод.

maps.Clone: «снимок состояния» для безопасной работы

Допустим, мы хотим держать индекс задач по ID:

package main

import (
	"fmt"
	"maps"
)

func main() {
	byID := map[int]string{1: "Buy milk", 2: "Learn Go"}
	snapshot := maps.Clone(byID)

	snapshot[1] = "Hacked"
	fmt.Println(byID[1])     // Buy milk
	fmt.Println(snapshot[1]) // Hacked
}

Это удобно, когда вы хотите сделать «снимок на момент времени» и дальше его анализировать, не боясь, что оригинал поменяется. При этом стоит помнить важную оговорку: клонирование мапы — это копирование пар ключ/значение, а не «глубокая копия» всех вложенных структур (если значения ссылочные, копируется ссылка). Это не проблема maps, это природа значений в Go.

maps.Copy: перенос данных в существующую мапу

Иногда вы хотите не создать новую мапу, а дополнить существующую. Тогда Copy выглядит проще, чем цикл.

package main

import (
	"fmt"
	"maps"
)

func main() {
	dst := map[int]string{1: "A"}
	src := map[int]string{2: "B", 3: "C"}

	maps.Copy(dst, src)
	fmt.Println(dst) // map[1:A 2:B 3:C]
}

maps.Equal: выразительное сравнение мап

Сравнение мап вручную — это ещё один вечный цикл «проверь длину, проверь все ключи, сравни значения…». Если значения сравнимы, maps.Equal решает задачу коротко и выразительно.

package main

import (
	"fmt"
	"maps"
)

func main() {
	a := map[string]int{"x": 1, "y": 2}
	b := map[string]int{"y": 2, "x": 1}

	fmt.Println(maps.Equal(a, b)) // true
}

4. cmp, стабильный вывод и момент, когда цикл честнее

cmp: меньше рутины в компараторах базовых типов

Пакет cmp — маленький, но очень приятный, особенно когда вы хотите сортировать или сравнивать значения и вам нужно меньше рутины. Его цель — дать стандартные функции сравнения для упорядочиваемых типов (например, int, string).

Самый практичный сценарий: вы сортируете []int или []string через slices.SortFunc, но не хотите писать компаратор вручную.

package main

import (
	"cmp"
	"fmt"
	"slices"
)

func main() {
	nums := []int{3, 1, 2}
	slices.SortFunc(nums, cmp.Compare[int])

	fmt.Println(nums) // [1 2 3]
}

Здесь красиво то, что cmp.Compare[int] — это уже готовая функция подходящего вида func(a, b int) int. То есть cmp не заменяет slices, а хорошо с ним стыкуется.

Если мы сортируем задачи по Priority, можно сделать компаратор чуть аккуратнее: сначала сравнить приоритет, а если равны — сравнить заголовок. Для второй части (строки) тоже можно использовать cmp.Compare[string].

package main

import (
	"cmp"
	"fmt"
	"slices"
)

type Task struct {
	Title    string
	Priority int
}

func main() {
	tasks := []Task{{"B", 2}, {"A", 2}, {"C", 1}}

	slices.SortFunc(tasks, func(a, b Task) int {
		if p := cmp.Compare(a.Priority, b.Priority); p != 0 {
			return p
		}
		return cmp.Compare(a.Title, b.Title)
	})

	fmt.Println(tasks) // [{C 1} {A 2} {B 2}]
}

maps + slices + cmp: стабильный вывод поверх map

Очень часто в приложениях нужно вывести данные из map человеку (или в лог), и вы внезапно обнаруживаете, что порядок «пляшет». Это не баг Go — это контракт map: порядок итерации не гарантируется. Поэтому типовой приём такой: достаём ключи, сортируем ключи, потом по отсортированным ключам печатаем значения.

Ниже — маленькая связка, где у нас map[int]Task, мы делаем стабильный вывод по ID.

package main

import (
	"cmp"
	"fmt"
	"maps"
	"slices"
)

type Task struct {
	Title string
}

func main() {
	byID := map[int]Task{2: {"Learn Go"}, 1: {"Buy milk"}}

	ids := maps.Keys(byID)
	slices.SortFunc(ids, cmp.Compare[int])

	for _, id := range ids {
		fmt.Println(id, byID[id].Title)
		// 1 Buy milk
		// 2 Learn Go
	}
}

И вот тут особенно видно, зачем это всё: код говорит сам за себя. Мы не спорим с map, мы просто строим стабильный вывод поверх неё.

Когда стандартное решение лучше, а когда цикл читаемее

Есть соблазн решить, что теперь «правильный Go» — это писать всё через slices и maps. На самом деле правильный Go — это код, который легко читать и сложно сломать.

Иногда slices.Delete действительно лучше, чем append с тремя срезами. А иногда обычный цикл лучше, чем функция с компаратором, потому что логика слишком доменная и вам нужно сделать что-то нестандартное по шагам.

Простой критерий «на пальцах» такой: если вы делаете типовую операцию (скопировать, удалить диапазон, отсортировать, сравнить, проверить наличие) и стандартная функция выражает намерение прямо — берите стандартную. Если операция уникальная, с кучей условий и частным поведением (например, «удалить задачи только если они done и старше N, но оставить хотя бы одну „последнюю done“ для отчёта») — цикл будет честнее, потому что он показывает детали.

Полезно помнить ещё одну вещь: стандартные пакеты писались не ради того, чтобы заменить мышление, а ради того, чтобы заменить рутину. Рутину лучше делегировать стандартной библиотеке. Мышление — оставляем себе (оно всё равно так просто не делегируется, даже за деньги).

5. Типичные ошибки при использовании slices/maps/cmp

Ошибка №1: забыли присваивание после slices.Delete (или похожей функции, которая возвращает слайс).
Такое происходит особенно часто у новичков, потому что визуально кажется: «я же удалил». Но удаление меняет длину слайса, а значит функция возвращает обновлённую «шапку» слайса. Если не сделать tasks = slices.Delete(tasks, i, j), вы просто выбросите результат и продолжите жить со старой длиной.

Ошибка №2: ожидание, что slices.Sort вернёт новый слайс.
Сортировка меняет порядок элементов «на месте» и ничего не возвращает. Из-за этого иногда пишут лишние переменные или пытаются присвоить результат: tasks = slices.Sort(tasks) — так не работает. Правильная модель в голове: «Sort меняет, Delete возвращает».

Ошибка №3: перепутали диапазон [i, j) и думали, что j включительно.
Delete(s, i, j) удаляет элементы с индексами i, i+1, …, j-1. Это абсолютно та же логика, что и у среза s[i:j]. Ошибка «на единицу» здесь особенно неприятна: код компилируется, но результат будет тихо неправильным.

Ошибка №4: попытка использовать cmp.Compare там, где тип не упорядочиваемый.
cmp.Compare хорош для int, string и других ordered-типов. Но если вы сортируете структуры, где порядок задаётся по полям, вам нужен свой компаратор. Это не недостаток cmp, а нормальная цена за то, что порядок у доменных сущностей почти всегда «наш», а не встроенный.

Ошибка №5: ожидание «глубокого копирования» от maps.Clone и slices.Clone.
Обе функции копируют структуру коллекции (элементы слайса или пары мапы). Но если внутри лежат указатели, слайсы, мапы, то копируются сами ссылки на эти объекты. Иногда это нормально, иногда нет — но важно не удивляться этому в середине отладки в 2 ночи.

1
Задача
Go SELF, 27 уровень, 2 лекция
Недоступна
Сортировка списка
Сортировка списка
1
Задача
Go SELF, 27 уровень, 2 лекция
Недоступна
Два вида порядка
Два вида порядка
1
Задача
Go SELF, 27 уровень, 2 лекция
Недоступна
Вырезать фрагмент
Вырезать фрагмент
1
Задача
Go SELF, 27 уровень, 2 лекция
Недоступна
Снимок настроек
Снимок настроек
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ