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
операції над []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, але залишити хоча б одну останню виконану задачу для звіту»), цикл буде чеснішим, бо він показує деталі.

Корисно пам’ятати ще одну річ: стандартні пакети писалися не для того, щоб замінити мислення, а для того, щоб прибрати рутину. Рутину краще делегувати стандартній бібліотеці. Мислення залишаємо собі — воно все одно так просто не делегується, навіть за гроші.

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.
Обидві функції копіюють структуру колекції — елементи слайса або пари ключ/значення мапи. Але якщо всередині лежать вказівники, слайси чи мапи, копіюються самі посилання на ці об’єкти. Іноді це нормально, іноді ні — але важливо не дивуватися цьому посеред налагодження о другій ночі.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ