Filtering a Go slice often starts as a small loop that keeps selected elements and discards the rest. slices.DeleteFunc expresses the inverse operation directly: a predicate marks elements for removal, the retained elements stay in their original order, and the existing backing array is reused.
That last property matters. DeleteFunc is not a copying filter. It mutates the supplied slice storage and returns a slice header with the resulting length.
The predicate selects elements for removal
The function accepts a slice and a deletion predicate:
func DeleteFunc[S ~[]E, E any](s S, del func(E) bool) SA predicate result of true removes the element. A result of false keeps it.
package main
import (
"fmt"
"slices"
)
func main() {
values := []int{2, 7, 4, 9, 6}
values = slices.DeleteFunc(values, func(v int) bool {
return v%2 != 0
})
fmt.Println(values)
}The result is:
[2 4 6]The retained values preserve their relative order. This makes the operation suitable when slice order carries meaning, unlike removal techniques that replace a deleted element with the final element.
The returned slice must replace the old header
Removing elements changes the slice length, so the returned value is part of the operation’s contract.
items = slices.DeleteFunc(items, expired)Calling DeleteFunc and ignoring its result leaves the original slice header unchanged even though its backing array has been modified. Code using that stale header can observe moved elements and zero values beyond the new logical end.
The same consideration applies to aliases. If two slice values refer to the same backing array, mutation through DeleteFunc is visible through both where their ranges overlap.
base := []string{"keep", "drop", "stay"}
alias := base
base = slices.DeleteFunc(base, func(s string) bool {
return s == "drop"
})
fmt.Println(base) // [keep stay]
fmt.Println(alias) // [keep stay ]The final empty string in alias is not an extra retained element. It is the zeroed tail observed through the old three-element slice header.
Removed tail slots are cleared
Current Go releases define DeleteFunc to zero the elements between the new length and the original length. For pointer-bearing element types, clearing those obsolete slots removes references that could otherwise keep objects reachable through the backing array.
type Record struct {
ID int
}
records := []*Record{
{ID: 10},
{ID: 20},
{ID: 30},
}
records = slices.DeleteFunc(records, func(r *Record) bool {
return r.ID == 20
})After the call, the logical slice contains the records with IDs 10 and 30. The obsolete slot at the tail is set to nil.
This tail-clearing behavior has been part of the standard-library contract since Go 1.22. Code written against older behavior should not depend on stale values remaining beyond the returned length.
Nilness is preserved for an empty result
If every element is removed, the returned slice has the same nilness as the input.
var nilSlice []int
nilSlice = slices.DeleteFunc(nilSlice, func(int) bool { return true })
fmt.Println(nilSlice == nil) // true
empty := []int{}
empty = slices.DeleteFunc(empty, func(int) bool { return true })
fmt.Println(empty == nil) // falseA non-nil input that becomes empty therefore remains non-nil. That detail can matter at serialization or API boundaries that distinguish a null slice from an empty slice.
In-place filtering has an ownership cost
DeleteFunc fits code that owns the slice storage or intentionally permits mutation. It avoids constructing a separate result slice solely to filter elements, but that also means callers must treat the original slice contents as modified.
When the input must remain intact, a separate destination states a different ownership contract. One common form is to append retained values into fresh storage:
filtered := make([]Item, 0, len(items))
for _, item := range items {
if keep(item) {
filtered = append(filtered, item)
}
}That form allocates independent outer slice storage. DeleteFunc instead compacts retained elements into the supplied backing array.
The distinction is more significant than syntax. Choosing between the two forms determines whether aliases may observe mutation and whether the original ordering data remains available after filtering.
Predicate side effects deserve care
The deletion predicate is best kept focused on classification. It receives each element value, not an index, and the operation is allowed to rearrange retained elements within the same backing array as it compacts the result.
A predicate that mutates shared state referenced by slice elements can make the filtering condition harder to reason about. For reference-bearing elements, the value passed to the predicate can still refer to mutable objects even though the slice element itself is passed by value.
The useful boundary for slices.DeleteFunc is therefore precise: it is an ordered, in-place removal operation driven by an element predicate. When storage ownership matches that contract, it replaces hand-written compaction code while also handling obsolete tail references explicitly.