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 ці пакети додали до стандартної бібліотеки.
Дуже корисно тримати в голові «карту намірів»:
| Пакет | Про що він | Типове відчуття в коді |
|---|---|---|
|
операції над []T | «Я роблю те саме, що й циклом, але коротше та читабельніше» |
|
операції над map[K]V | «Я копіюю, порівнюю або дістаю ключі без ручного for range» |
|
порівняння впорядковуваних значень | «Я задаю порядок через готові порівняння, з меншою кількістю ручної рутини» |
Окрема «магія», яку важливо розуміти, коли читаєте чужий код: у сигнатурах 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.
Обидві функції копіюють структуру колекції — елементи слайса або пари ключ/значення мапи. Але якщо всередині лежать вказівники, слайси чи мапи, копіюються самі посилання на ці об’єкти. Іноді це нормально, іноді ні — але важливо не дивуватися цьому посеред налагодження о другій ночі.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ