1. Чим хороший пакет slices
Коли ви вперше вчитеся видаляти фрагмент зі слайса, рецепт через append(s[:i], s[j:]...) здається магією: «як це взагалі працює і чому тут три крапки?!». Потім до нього звикають, але магія не зникає — вона просто переходить із категорії «незрозуміло» в категорію «на автоматі». А саме на автопілоті й народжуються баги: переплутали межі, забули присвоїти результат, випадково перезаписали дані.
Пакет slices — це спроба зробити код самодокументованим: не «ось вам трюк з append», а «ось вам Delete — видалити діапазон». Такі функції працюють зі слайсами будь-якого типу (завдяки узагальненням, або generics), і важливо вміти читати їх та правильно застосовувати, навіть якщо ви не збираєтеся писати власні узагальнені утиліти.
Щоб зафіксувати думку, подивімося на одну й ту саму операцію у двох стилях:
| Що потрібно зробити | «Рецепт» вручну | Стандартно через slices |
|---|---|---|
| Видалити діапазон [i:j) | |
|
| Вставити елементи в позицію i | |
|
| Зробити незалежну копію | |
|
Сенс не в тому, що стандартна бібліотека «розумніша за вас». Сенс у тому, що вона зменшує когнітивне навантаження: менше «трюків», більше «намірів».
2. Головне правило: якщо змінюється довжина — зберігаємо результат
Пакет slices спеціально побудований так, щоб повторювати звичну модель append. Якщо операція потенційно змінює len (а інколи ще й перевиділяє backing array), вона повертає новий слайс, і його треба зберегти. Саме тому slices.Delete і slices.Insert повертають значення.
Звідси просте правило, яке економить години життя: якщо функція змінює довжину — пишемо s = …. І не з естетики, а тому, що інакше в змінної залишиться стара довжина, а вміст backing array уже може бути частково перезаписаний. Тоді ви отримаєте «зомбі-слайс»: зовні він начебто живий, а всередині вже все не так.
Міні-демонстрація «зомбі-слайса»:
package main
import (
"fmt"
"slices"
)
func main() {
s := []int{1, 2, 3, 4, 5}
slices.Delete(s, 1, 4) // забули присвоїти результат
fmt.Println(s) // вміст може стати несподіваним
}
Ця програма компілюється, запускається, інколи навіть «схожа на робочу» — і саме тому небезпечна.
3. Практика: todo-список на []string
Щоб приклади не були надто абстрактними, розглянемо маленьке консольне ядро: список справ (todo), де кожна справа — просто рядок. Без структур, без бази даних і без мікросервісів — сьогодні святкуємо простоту.
Ідея така: у нас є tasks := []string{…}, і ми хочемо вміти:
- безпечно копіювати список (наприклад, перед експериментами),
- видаляти одну справу або діапазон,
- вставляти справи в середину (наприклад, термінові задачі).
slices.Clone: «скопіювати по-справжньому»
Дуже поширена помилка новачків зі слайсами звучить так: «я скопіював слайс b := a, а потім змінив b, і чому ж змінився a?». Відповідь проста: ви скопіювали заголовок слайса, а не дані. Обидва слайси й далі ділять спільний backing array — тобто ви зробили не копію списку, а ще одне вікно в ті самі дані.
Clone розв’язує саме цю задачу: створює новий backing array і переносить туди елементи.
Приклад: робимо чернетку, не чіпаючи оригінал.
package main
import (
"fmt"
"slices"
)
func main() {
tasks := []string{"buy milk", "read book", "walk dog"}
draft := slices.Clone(tasks)
draft[0] = "buy coffee"
fmt.Println(tasks) // [buy milk read book walk dog]
fmt.Println(draft) // [buy coffee read book walk dog]
}
Тут draft — незалежна копія: змінюємо її як хочемо, tasks не змінюється.
Трохи життєвіший сценарій: припустімо, ви хочете «спробувати» фільтрацію або видалення, але не впевнені, що правильно порахували індекси. Можна клонувати, поекспериментувати, а потім застосувати зміни до оригіналу — або й зовсім відмовитися від них. У реальному коді так роблять не завжди: пам’ять не безплатна. Але як техніка безпеки для новачка це дуже корисно.
slices.Delete: видалити діапазон [i:j)
Коли ми видаляємо елементи зі слайса, фактично робимо дві речі: зсуваємо хвіст на місце «дірки» і зменшуємо довжину. Раніше це доводилося писати вручну. Тепер можна сказати прямо: Delete. І це читається як англійське речення: «delete from i to j».
Важливо пам’ятати, що діапазон напіввідкритий: [i:j) означає, що i входить, а j — ні. Це та сама модель, що й у s[i:j].
Приклад: видалити одну справу за індексом i.
package main
import (
"fmt"
"slices"
)
func main() {
tasks := []string{"buy milk", "read book", "walk dog"}
i := 1
tasks = slices.Delete(tasks, i, i+1)
fmt.Println(tasks) // [buy milk walk dog]
}
Приклад: видалити діапазон (наприклад, «знести блок задач», який уже не актуальний).
package main
import (
"fmt"
"slices"
)
func main() {
tasks := []string{"A", "B", "C", "D", "E"}
tasks = slices.Delete(tasks, 1, 4)
fmt.Println(tasks) // [A E]
}
Після Delete старий слайс краще не вважати «стабільним»
Delete зазвичай зсуває елементи на місці, тобто backing array перевикористовується, якщо не потрібна реалокація. Тому початковий слайс після модифікувальної операції краще вважати «невалідним» у сенсі припущень про його довжину й вміст, якщо ви ігноруєте значення, що повертається, або й далі користуєтеся старими «видами» на нього.
Погана ідея виглядає так: «я зроблю u := slices.Delete(s, …), а потім і далі активно використовуватиму s, ніби нічого не сталося». Особливо небезпечно це тоді, коли поруч уже є підслайси, які ділять той самий backing array.
Про хвіст і пам’ять: Delete намагається очищати звільнені елементи
Історично тут була тонка проблема: після видалення елементи могли залишатися в хвості backing array (за межами нового len) і утримувати пам’ять, якщо там лежали посилання. Починаючи з Go 1.22, поведінку низки функцій пакета slices змінювали в бік безпечнішої роботи з таким хвостом, зокрема обнуляли звільнені елементи.
Це хороша новина, але вона не скасовує дисципліну: результат усе одно треба зберігати, а індекси — рахувати правильно.
slices.Insert: вставити елементи в позицію i
Вставка — симетрична операція до видалення. Якщо видалення «схлопує дірку», то вставка «створює дірку»: зсуває хвіст праворуч і розширює len. Раніше це робили через append + copy. Тепер можна прямо показати намір: Insert.
Insert приймає позицію та варіадичні аргументи (елементи для вставки). Він повертає новий слайс, бо довжина змінюється, а backing array може переалокуватися.
Приклад: вставити одну термінову справу на початок.
package main
import (
"fmt"
"slices"
)
func main() {
tasks := []string{"read book", "walk dog"}
tasks = slices.Insert(tasks, 0, "pay rent")
fmt.Println(tasks) // [pay rent read book walk dog]
}
Приклад: вставити відразу кілька справ у середину.
package main
import (
"fmt"
"slices"
)
func main() {
tasks := []string{"A", "D"}
tasks = slices.Insert(tasks, 1, "B", "C")
fmt.Println(tasks) // [A B C D]
}
Як це читати: «встав у tasks на позицію 1 елементи „B“ і „C“». Без copy, без ручного розширення, без ризику переплутати i+k.
Три правила безпечного застосування Delete/Insert/Clone
Коли ви вперше починаєте користуватися slices, здається: «ура, тепер усе безпечно й помилитися вже неможливо». На жаль, помилитися можна завжди. Але ризик можна суттєво знизити, якщо тримати в голові три правила.
Перше правило — значення, що повертається, обов’язково зберігаємо, якщо функція змінює довжину. Для Delete і Insert це не стиль, а коректність.
Друге правило — обережно з індексами та напівінтервалами. Delete(s, i, j) видаляє саме [i:j). Якщо ви на автопілоті думаєте «включно», видалите на один елемент менше або більше. А «на один елемент» — це улюблений розмір катастрофи в програмуванні.
Третє правило — не множте старі версії слайса і не сподівайтеся, що вони й далі відображають реальність. Якщо у вас були підслайси і ви зробили Delete/Insert у початковому слайсі — подумайте, що тепер відбувається з підслайсами. Backing array міг зсунутися, елементи могли переїхати, хвіст міг обнулитися.
Міні-рефакторинг: функції для todo-ядра
Зараз зберемо маленький набір функцій навколо []string, щоб код у main виглядав по-людськи. Це хороший стиль: операції над даними оформлюємо окремими функціями, а в main просто показуємо сценарій.
Видалення задачі за індексом із захистом від неправильного i
package main
import "slices"
func removeTask(tasks []string, i int) []string {
if i < 0 || i >= len(tasks) {
return tasks
}
return slices.Delete(tasks, i, i+1)
}
Зверніть увагу, як читається останній рядок: «видали з i по i+1». Тобто один елемент.
Вставка задачі «після i»
package main
import "slices"
func insertAfter(tasks []string, i int, task string) []string {
pos := i + 1
if pos < 0 || pos > len(tasks) {
return tasks
}
return slices.Insert(tasks, pos, task)
}
Ми акуратно допускаємо pos == len(tasks) — це означає «вставити в кінець», і це коректно.
Чернетка (clone) перед експериментом
package main
import "slices"
func draftCopy(tasks []string) []string {
return slices.Clone(tasks)
}
Мінімалістично й по суті.
Сценарій у main
package main
import "fmt"
func main() {
tasks := []string{"A", "B", "C"}
copyForTest := draftCopy(tasks)
tasks = removeTask(tasks, 1)
tasks = insertAfter(tasks, 0, "X")
fmt.Println("tasks:", tasks) // tasks: [A X C]
fmt.Println("draft:", copyForTest) // draft: [A B C]
}
Тут одразу видно, що Clone справді захищає оригінал: чернетка залишилася незмінною.
slices як спільний словник у команді
Є неочевидний, але дуже практичний плюс стандартних функцій: вони формують спільний словник. Коли ви бачите slices.Delete(s, 2, 5), вам майже не потрібно думати про механіку — ви читаєте намір.
Це особливо важливо в команді, де в людей різний досвід: хтось жонглює append із заплющеними очима, а хтось іще плутає [i:j).
4. Типові помилки під час роботи з slices.Delete/Insert/Clone
Помилка №1: викликати slices.Delete або slices.Insert і не присвоїти результат назад.
Це найпопулярніший баг, тому що код компілюється і виглядає правдоподібно. Проблема в тому, що довжина змінної s не змінюється, а backing array уже міг бути частково перезаписаний. Правильна звичка: якщо операція змінює довжину, пишемо s = slices.Delete(...) і s = slices.Insert(...).
Помилка №2: випадково зробити затінення (shadowing) через := замість =.
Класика жанру: всередині if або for ви пишете s := slices.Delete(s, 2, 3), думаєте, що оновили s, а насправді створили нову змінну s лише всередині блока. Зовні все лишилося старим, і ви потім довго дивитеся на результат та підозрюєте, що Go жартує.
Помилка №3: переплутати межі й думати, що j входить.
У Delete(s, i, j) видаляється діапазон [i:j), права межа не входить. Якщо ви хочете видалити один елемент i, пишемо Delete(s, i, i+1). Якщо хочете видалити «включно», потрібно самостійно перетворити це на напівінтервал (зазвичай j+1) і дуже уважно стежити, щоб не вийти за len.
Помилка №4: продовжувати активно використовувати старі підслайси після Delete/Insert.
Якщо десь був head := s[:k] або tail := s[k:], а потім ви зробили s = slices.Delete(s, ...), то head і tail можуть раптово почати показувати несподівані дані, тому що backing array перевикористано, елементи зсунуто, а хвіст міг бути очищений. Це не «магія», а модель aliasing: кілька слайсів можуть ділити один backing array.
Помилка №5: думати, що slices.Clone — це те саме, що b := a.
b := a копіює лише «заголовок» (pointer/len/cap), а не елементи. slices.Clone(a) робить незалежну копію даних, виділяючи новий backing array.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ