Удалить элемент из слайса в Go можно тремя способами. Два сохраняют порядок, третий работает за O(1). Самый распространённый при этом подтекает памятью на слайсе указателей.
Классика из SliceTricks:
s = append(s[:i], s[i+1:]...)
Порядок элементов сохраняется, сложность O(n).
С Go 1.21 почти то же самое есть в стандартной библиотеке:
s = slices.Delete(s, i, i+1)
Советую именно slices.Delete. Причина не в красоте, а в памяти. Разберу ниже.
Если порядок не важен: O(1)
Поставьте последний элемент на место удаляемого и обрежьте хвост:
s[i] = s[len(s)-1]
s[len(s)-1] = nil // если элементы — указатели, см. ниже
s = s[:len(s)-1]
Так удаление из середины миллионного слайса стоит столько же, сколько удаление с конца. Приём уместен, когда слайс — это множество или пул, а не упорядоченный список.
Утечка памяти при ручном append
Вот главная причина не писать append(s[:i], s[i+1:]...) руками.
Слайс — это указатель на участок памяти, длина и ёмкость. append сдвигает элементы влево и уменьшает длину. Ёмкость не меняется, и хвост массива за новой длиной остаётся нетронутым. Там всё ещё лежит старая ссылка.
Проявляется это при удалении последнего элемента. Сдвигать нечего, длина просто уменьшается, а слот остаётся заполненным:
type Session struct {
Buf [1 << 20]byte // мегабайт на сессию
}
sessions := []*Session{s0, s1, s2}
sessions = append(sessions[:2], sessions[3:]...)
// len == 2, cap == 3
// подлежащий массив всё ещё [s0, s1, s2]
Сборщик мусора видит живую ссылку из массива и не собирает s2. Мегабайт висит, пока жив весь слайс. На слайсе указателей, который живёт долго и постепенно укорачивается, это медленная утечка памяти.
Я проверил это через runtime.AddCleanup и принудительный runtime.GC(). При удалении последнего элемента через append не собирается ничего. Тот же случай через slices.Delete освобождает объект.
Начиная с Go 1.22 slices.Delete обнуляет освободившийся хвост. Ссылки исчезают, GC собирает объекты.
При удалении из середины утечки нет. Туда копируются живые элементы, а в последнем слоте остаётся дубликат указателя, который и так жив. Но код удаления обычно не знает, какой индекс ему передадут.
С приёмом за O(1) та же история, поэтому обнулять хвост нужно до обрезки. Написать s[len(s)] = nil после неё не выйдет: индексация за пределами длины паникует, даже если ёмкость позволяет.
Если тип элемента не содержит указателей (числа, структуры из чисел), проблемы нет. Но тип со временем меняется, а код удаления остаётся. Проще сразу использовать slices.Delete.
Удаление по значению
В стандартной библиотеке нет функции «удали вот этот элемент». Сначала найдите индекс:
if i := slices.Index(s, target); i >= 0 {
s = slices.Delete(s, i, i+1)
}
slices.Index вернёт -1, если элемента нет. Проверка обязательна: slices.Delete с отрицательным индексом паникует.
Для сложных структур, где сравнение по == не работает, есть slices.IndexFunc:
i := slices.IndexFunc(users, func(u User) bool { return u.ID == id })
Удаление нескольких элементов
slices.Delete принимает диапазон, а не один индекс. Идущие подряд элементы удаляются одним вызовом за O(n):
s = slices.Delete(s, 2, 5) // удалит элементы 2, 3, 4
Если элементы разбросаны и выбираются по условию, не вызывайте Delete в цикле. Это O(n²). Вдобавок ломаются индексы: после каждого удаления остальные сдвигаются, и цикл перескакивает через соседа удалённого. А for i := range s ещё и паникует. Число итераций фиксируется по исходной длине. На последних шагах индекс уходит за конец укоротившегося слайса. Для такой задачи есть slices.DeleteFunc за один проход. Об этом я писал в заметке про фильтрацию слайса.
Что выбрать
| Задача | Способ | Сложность |
|---|---|---|
| Удалить по индексу, порядок важен | slices.Delete(s, i, i+1) | O(n) |
| Порядок не важен | swap с последним, обнулить хвост | O(1) |
| Удалить по значению | slices.Index + slices.Delete | O(n) |
| Удалить по условию | slices.DeleteFunc | O(n) |
Обратная операция — вставка элемента в слайс. Что ещё почитать про базовые типы Go, я собрал в дорожной карте.
Теги: