Как удалить элемент из слайса в Go: три способа и утечка памяти

• обновлено • 3 мин чтения

Удалить элемент из слайса в 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.DeleteO(n)
Удалить по условиюslices.DeleteFuncO(n)

Обратная операция — вставка элемента в слайс. Что ещё почитать про базовые типы Go, я собрал в дорожной карте.


Об авторе: Александр Бруяко — руководитель разработки с опытом в backend, инфраструктуре и управлении командами.


Теги: