Delete an Element From a Slice in Go Without Leaking It
Remove an element from a Go slice with slices.Delete, keep or ignore the order, and see why the old append trick leaves a stale pointer behind the length.

A Go slice has no Remove method, and the first answer people find online is a one-line append that works and quietly leaves something behind. The tidy answer today is in the standard library. This is the whole decision in one place: which form, what it costs, and what the array behind the slice looks like afterwards. If you want the model that explains the tail, read slices from the header to the backing array first.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
Delete and keep the order
package main
import (
"fmt"
"slices"
)
func main() {
s := []string{"a", "b", "c", "d", "e"}
s = slices.Delete(s, 1, 3)
fmt.Println(s, len(s), cap(s))
full := s[:cap(s)]
fmt.Printf("%q\n", full)
}[a d e] 3 5
["a" "d" "e" "" ""]slices.Delete(s, 1, 3) removes the elements at indexes 1 and 2, moves the rest down and returns a slice of length 3. The capacity does not shrink, and the tail of the array (the last two slots) is cleared to the zero value, which is what the second print shows by reslicing up to the capacity. Assign the result back, because the original s still has its old length. The call panics if the indexes are out of range, so check i < len(s) first when the index comes from outside.
The old idiom and what it leaves behind
Before Go 1.21 you wrote this, and many codebases still do:
package main
import "fmt"
type Conn struct{ ID int }
func main() {
conns := []*Conn{{1}, {2}, {3}, {4}}
conns = append(conns[:1], conns[2:]...)
fmt.Println(len(conns), cap(conns))
for i, c := range conns[:cap(conns)] {
fmt.Println(i, c.ID)
}
}3 4
0 1
1 3
2 4
3 4The slice has three elements and the array has four slots. The fourth slot, past the length, still holds a pointer to the last connection, and the loop over the full capacity prints its ID twice, once at index 2 and again at index 3. The runtime treats that pointer as live, so for a slice of large objects it keeps the removed value alive for as long as the array lives. That is the leak, and it is harmless for small integers and expensive for []*Session or []*Buffer.
If you write the loop yourself, assign the zero value to the vacated slot: conns[len(conns)-1] = nil before shrinking, or clear(conns[len(conns):cap(conns)]) after. slices.Delete and slices.DeleteFunc do this for you since Go 1.22, so switching to them removes the problem along with the code.
When order does not matter
If the slice is a set or a pool, move the last element into the hole and shorten by one:
s[i] = s[len(s)-1]
s[len(s)-1] = ""
s = s[:len(s)-1]That is constant time, with no shifting, which matters when the slice is long and deletions are frequent. It reorders the slice, so do not use it if anything depends on position, such as a list rendered to a user or an index stored elsewhere. If you iterate and delete in the same loop, go backwards or build a filtered result, because deleting while ranging forward skips the element that moves into the hole. Filtering many elements is better done in one pass with slices.DeleteFunc(s, func(x T) bool { return ... }).
Where this does not fit
If you delete from the middle of a large slice constantly, the shifting dominates, and a different structure might be worth measuring. A map keyed by ID gives constant time removal and loses order, and a linked list has poor cache behavior in Go and rarely wins. For a queue, s = s[1:] is fine for short-lived queues, but it never frees the front of the array, so a long-running one should copy to a fresh slice periodically. Copying a slice the safe way shows how, and the reference for the helpers is the slices package documentation and the Go 1.22 release notes, which describe the zeroing change.


