1. Що робить clear і навіщо він потрібен
Якщо ви щойно звикли до append, copy і s[a:b], то clear спершу здається ще одним магічним інструментом у скриньці Go. Насправді він вирішує цілком практичну проблему: ми вміємо зменшувати len, тобто робити s = s[:newLen], але це не стирає значення з backing array. Іншими словами, елементи ми прибираємо з поля зору, а самі вони нікуди не діваються — як забута піца в холодильнику: ви її не бачите, зате за день вона про себе нагадає.
clear — це вбудована функція (built-in). Для слайса вона записує zero value в елементи заданого діапазону. Важливо відразу зафіксувати: clear не змінює len і cap. Він нічого не відрізає — просто протирає поверхню.
Такий підхід дуже типовий для Go: мова часто додає невеликі вбудовані механізми, які прибирають повторюваний шаблонний код і роблять поведінку передбачуванішою. Схожа історія була й з append: колись для цього писали ручні addToList з make + copy, а потім це зібрали в один зрозумілий інструмент.
Міні-приклад: clear не робить слайс порожнім
package main
import "fmt"
func main() {
s := []int{10, 20, 30}
clear(s)
fmt.Println(s) // [0 0 0]
fmt.Println(len(s)) // 3
}
2. Zero value і очищення посилань
Зараз буде момент, де Go поводиться як суворий учитель математики: «не фантазуйте, запамʼятайте визначення». clear записує zero value для типу елементів.
Для int це 0, для bool — false, для string — "". Для «посилальних» типів, наприклад []int або map[string]int, zero value зазвичай виглядає як nil. І це критично: коли ви обнуляєте елемент посилального типу, ви прибираєте посилання на дані. А це вже розмова не про «красу виводу», а про памʼять і збирач сміття (GC).
Поки що не занурюватимемося в GC глибоко. Нам вистачить простої практичної моделі:
Якщо десь у програмі залишається посилання на дані, GC вважає, що вони ще потрібні, і не звільняє памʼять.
clear — один зі способів акуратно прибрати такі посилання, коли ви логічно «видалили» елемент із колекції.
Таблиця: clear для різних типів елементів
| Тип елемента T | Zero value після clear | Що це означає на практиці |
|---|---|---|
|
|
Число занулено (без ефекту для памʼяті, окрім самої комірки) |
|
|
Прапорець скинуто |
|
|
Прибрали посилання на дані рядка — це важливо для памʼяті |
|
|
Прибрали посилання на backing array іншого слайса |
|
|
Прибрали посилання на хеш-таблицю |
У map ми поки що не заглиблюємося; просто зафіксуймо, що clear — це не «тільки для []T».
Міні-приклад: clear очищає частину діапазону
package main
import "fmt"
func main() {
s := []string{"A", "B", "C", "D"}
clear(s[1:3])
fmt.Println(s) // [A D]
// точніше: ["A" "" "" "D"]
}
3. Чому зменшення len не стирає хвіст
Тепер перейдемо до головної причини, чому після видалення зі слайса часто викликають clear. Саме тут і ховається один із найчастіших «примарних» багів зі слайсами.
У слайса є len і cap. len показує, яка частина backing array зараз видима. cap показує, наскільки далеко backing array загалом простягається від початку цього слайса.
Те, що лежить за len, але в межах cap, зазвичай називають «хвостом». Звичайним індексуванням s[i] його не видно, бо індекси перевіряються за len. Але хвіст реально існує в памʼяті backing array.
Ось модель для уяви:
backing array: [ A ][ B ][ C ][ D ][ ? ][ ? ]
slice s: ^----------------^
len=4 cap=6
tail (хвіст): [ ? ][ ? ] (це частина backing array, але за len)
І тепер важливий момент: якщо ви зробили s = s[:2], backing array не стискається і не стирається. Ви просто змінюєте вікно перегляду.
Міні-приклад: хвіст можна побачити під час розширення слайса
Цей приклад трохи штучний — у реальному коді s[:cap(s)] пишуть рідко. Проте механіку він показує чудово.
package main
import "fmt"
func main() {
s := []string{"A", "B", "C", "D"}
s = s[:2] // видимо тільки A і B
t := s[:cap(s)] // розширили видимість до cap
fmt.Println(t) // [A B C D]
}
Мораль проста: «видалити» елемент через зменшення len — не те саме, що «стерти» значення в backing array.
До речі, у попередніх темах ви вже бачили інструмент контролю cap через повний вираз зрізу s[a:b:c]. Його додали саме для того, щоб безпечніше керувати доступом до backing array і його хвоста.
4. Навіщо чистити хвіст: баги й памʼять
Легко сказати: «Ну й гаразд, хвіст лежить і лежить». Але є дві причини, через які після видалення хвіст варто очищати, — і обидві на практиці регулярно даються взнаки.
Перша проблема — логічна. Після видалення ви часто очікуєте, що «видаленого» більше ніде немає. Але якщо десь помилково станеться reslice або ви передасте підслайс із більшим cap, а хтось потім зробить append, старе значення може несподівано «спливти». Це не магія, а просто той самий хвіст, який ви не протерли.
Друга проблема — памʼять. Це особливо важливо, коли елементи слайса тримають посилання на великі дані: рядки, інші слайси, мапи. Якщо ви «видалили» елемент, але посилання на нього залишилося в хвості backing array, то GC продовжить вважати, що дані ще потрібні, і памʼять може не звільнятися. У підсумку код логічно «прибрав» дані, а з погляду памʼяті — не зовсім.
Можна думати так: зменшення len означає «ми більше не використовуємо», а clear — «і посилань теж більше немає».
5. Видалення елемента: copy + clear + новий len
Зараз ми зробимо головний практичний висновок: після видалення, особливо після стабільного видалення, коли ми зсуваємо хвіст ліворуч через copy, у кінці залишається дублікат. Цей «останній елемент старої довжини» вже не потрібен логічно, але фізично там лишається старе значення.
Тому типовий шаблон такий:
- Зсуваємо елементи ліворуч (copy).
- Занулюємо останній елемент старої довжини (clear за діапазоном з одного елемента).
- Укорочуємо len (s = s[:len(s)-1]).
Міні-приклад: стабільне видалення одного елемента
package main
import "fmt"
func removeAtStable(s []string, i int) []string {
if i < 0 || i >= len(s) {
return s
}
copy(s[i:], s[i+1:]) // зсув хвоста ліворуч
clear(s[len(s)-1 : len(s)]) // обнулили останній слот
return s[:len(s)-1] // зменшили len
}
func main() {
tasks := []string{"eat", "sleep", "code", "repeat"}
tasks = removeAtStable(tasks, 1)
fmt.Println(tasks) // [eat code repeat]
}
Зверніть увагу на clear(s[len(s)-1 : len(s)]): це діапазон довжини 1. Так, виглядає трохи канцелярськи, зате дуже прозоро: ми очищаємо рівно один елемент.
Якщо вам хочеться коротше — і ви впевнені, що добре розумієте межі, — можна написати clear(s[len(s)-1:]). Але це вже інша ідея: «очистити хвіст до cap». Для нашої поточної ситуації частіше потрібно очистити саме той «звільнений слот».
6. Видалення діапазону: чистимо s[newLen:oldLen]
Видалення діапазону [i:j) — це те саме, тільки «дірка» ширша. Ми закриваємо її хвостом через copy(s[i:], s[j:]). Після цього в кінці лишається кілька зайвих елементів, рівно j-i штук, — і саме їх добре очистити.
Тут зручно мислити двома довжинами: старою oldLen і новою newLen. Поки ви не вкоротили len, діапазон для clear легко вказати явно.
Міні-приклад: видалення діапазону + очищення хвоста
package main
import "fmt"
func deleteRange(s []string, i, j int) []string {
if i < 0 {
i = 0
}
if j > len(s) {
j = len(s)
}
if i >= j {
return s
}
oldLen := len(s)
moved := copy(s[i:], s[j:]) // скільки реально переїхало
newLen := i + moved
clear(s[newLen:oldLen]) // очищаємо саме «звільнену» частину
return s[:newLen]
}
func main() {
s := []string{"A", "B", "C", "D", "E", "F"}
s = deleteRange(s, 2, 5)
fmt.Println(s) // [A B F]
}
Тут важливо, що clear робиться до s = s[:newLen] (або зі збереженням oldLen). Інакше ви втратите зручний доступ до діапазону старого хвоста.
7. Приклад: міні-менеджер задач
Давайте продовжимо нашу мініісторію застосунку. Нехай у нас є найпростіший менеджер задач: список задач зберігається як []string. Ми вміємо друкувати задачі, додавати їх і видаляти за індексом.
Зараз ми додамо видалення так, щоб воно не залишало «висячих» посилань у хвості backing array.
Крок 1: друк задач
package main
import "fmt"
func printTasks(tasks []string) {
for i, t := range tasks {
fmt.Printf("%d) %s\n", i, t)
}
}
func main() {
tasks := []string{"read", "write", "delete tail carefully"}
printTasks(tasks)
// 0) read
// 1) write
// 2) delete tail carefully
}
Крок 2: видалення за індексом з clear
package main
import "fmt"
func removeTask(tasks []string, i int) []string {
if i < 0 || i >= len(tasks) {
return tasks
}
copy(tasks[i:], tasks[i+1:])
clear(tasks[len(tasks)-1 : len(tasks)])
return tasks[:len(tasks)-1]
}
func main() {
tasks := []string{"read", "write", "sleep", "repeat"}
tasks = removeTask(tasks, 2)
fmt.Println(tasks) // [read write repeat]
}
Тут приємно те, що функція повертає новий слайс. Це прямо відповідає правилу довжини: якщо операція змінює len, той, хто викликає, має отримати оновлений слайс. Інакше ви майже напевно прийдете до дивного стану: «всередині змінилося, а зовні — ні».
Схема процесу видалення й очищення
Іноді мозку простіше один раз побачити, ніж пʼять разів прочитати. Ось блок-схема процесу «stable remove + clear tail»:
flowchart TD
A[Є слайс tasks, len=n] --> B["Зсуваємо хвіст: copy(tasks[i:], tasks[i+1:])"]
B --> C[У кінці лишається дублікат останнього елемента]
C --> D["Очищаємо останній слот: clear(tasks[n-1:n])"]
D --> E["Укорочуємо len: tasks = tasks[:n-1]"]
E --> F[Готово: порядок збережено, хвіст не тримає посилань]
8. Типові помилки під час clear після видалення
Помилка №1: очікувати, що clear змінить довжину.
Дуже часта логічна пастка: ви робите clear(s) і чекаєте, що «слайс став порожнім». Але clear взагалі не про розмір. Він про значення. Якщо потрібен слайс, порожній за довжиною, то це s = s[:0]. Якщо потрібен слайс, порожній за значеннями, але тієї ж довжини, — це якраз clear(s).
Помилка №2: очищати не той діапазон через межі [a:b).
Коли ви чистите хвіст, майже завжди йдеться про напівінтервал. Наприклад, останній елемент старої довжини — це [oldLen-1 : oldLen], а не [oldLen-1 : oldLen-1] (це порожньо) і не [oldLen : oldLen+1] (panic). Найнадійніше на перших порах — зберігати oldLen і писати діапазони явно, навіть якщо це виглядає трохи довше.
Помилка №3: забути, що видалення — це мінімум дві дії, а clear — окрема.
Іноді пишуть copy(s[i:], s[i+1:]) і думають, що «видалили». Але довжина ж стара, і останній елемент повторюватиметься. Потім додають s = s[:len(s)-1] і вважають задачу закритою. Для []int це часто нормально, а для []string або [][]byte ви непомітно лишаєте посилання в хвості. Якщо в коді ви зменшили len, корисно звичкою ставити собі запитання: «А хвіст треба протерти?».
Помилка №4: робити clear після того, як ви втратили доступ до хвоста.
Якщо ви спочатку зробили s = s[:newLen], а стару довжину ніде не зберегли, то очистити «звільнену частину» вже не вийде через clear(s[newLen:oldLen]) — у вас просто немає oldLen. Так, можна чистити до cap, але це вже інше рішення і не завжди саме те, що ви хотіли. У функціях видалення часто простіше спочатку запамʼятати oldLen, потім викликати clear, і лише після цього змінювати len.
Помилка №5: ігнорувати утримання памʼяті так, ніби це міф.
Коли даних небагато, ви не побачите проблеми. Коли в слайсі лежать великі рядки або великі під-слайси, «видалили, але не очистили хвіст» перетворюється на витік за відчуттями: памʼять не повертається, процес роздувається, а ви сумуєте й підозрюєте Go в усіх смертних гріхах. Зазвичай винен не Go, а той самий хвіст, який ви полінувалися занулити.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ