Removing one or more adjacent elements from a Go slice used to mean writing the reslicing and append expression by hand. slices.Delete gives that operation a direct name and handles the vacated tail slots for you.
The call uses the same half-open range convention as slicing: slices.Delete(s, i, j) removes indexes i through j-1. The returned slice is shorter, so keep the return value.
Remove a range with slices.Delete
A basic deletion looks like this:
package main
import (
"fmt"
"slices"
)
func main() {
names := []string{"api", "cache", "debug", "trace", "worker"}
names = slices.Delete(names, 2, 4)
fmt.Println(names)
}The result is:
[api cache worker]Indexes 2 and 3 are removed. The element that was at index 4 shifts left to close the gap.
The range is half-open, just like names[2:4]. That makes the count of removed elements j - i, which is useful when the indexes come from another calculation.
Keep the slice returned by Delete
slices.Delete changes the contents of the existing slice and returns a new slice header with a shorter length. Code that ignores the return value still has the old length.
Use this form:
items = slices.Delete(items, start, end)Not this:
slices.Delete(items, start, end)The second call can still rearrange the backing array, but items keeps its previous length. That combination is especially confusing because printing items can expose shifted values followed by zero values in the slots that were cleared.
Returning the shorter slice also makes the operation compose naturally with later slice operations:
items = slices.Delete(items, start, end)
items = append(items, replacement...)At that point, append starts from the correct logical length.
Delete one element with a one-slot range
To remove the element at index i, delete [i:i+1]:
queue := []string{"job-a", "job-b", "job-c"}
queue = slices.Delete(queue, 1, 2)
fmt.Println(queue)Output:
[job-a job-c]This is clearer than rebuilding the expression from two subslices each time. The index still needs to be valid, so code receiving an index from a search should check the result before deletion:
i := slices.Index(queue, target)
if i >= 0 {
queue = slices.Delete(queue, i, i+1)
}Passing -1 from an unsuccessful search into Delete is not a sentinel operation. It creates an invalid slice range and panics.
An empty range removes nothing
When i == j, the range contains no elements. slices.Delete leaves the slice length and elements unchanged:
values := []int{10, 20, 30, 40}
values = slices.Delete(values, 1, 1)
fmt.Println(values)Output:
[10 20 30 40]This can be handy when the start and end positions come from normal range calculations. There is no need for a separate branch solely to handle an empty valid range.
The indexes still have to describe a valid slice boundary. For a slice of length four, (4, 4) is valid, while (5, 5) is not.
Invalid ranges panic
Delete follows normal Go slice-bound rules. Calls panic when s[i:j] would be invalid, including cases where the start is negative, the end exceeds the slice length, or the start is greater than the end.
For example, these calls are invalid for a three-element slice:
items = slices.Delete(items, -1, 1)
items = slices.Delete(items, 1, 4)
items = slices.Delete(items, 2, 1)Bounds coming from trusted program structure are often straightforward. Bounds derived from requests, file data, or other external input need validation before they reach the slice operation.
A compact check for integer indexes is:
if start < 0 || end < start || end > len(items) {
return fmt.Errorf("invalid item range")
}
items = slices.Delete(items, start, end)Keeping that validation next to the operation also makes it easier to distinguish malformed input from a programming error.
Delete shifts later elements in place
Removing a range means the elements after j move toward the front of the same backing array. That has two practical effects.
First, deletion costs more when a large suffix has to move. The documented complexity is proportional to the portion from the deletion point toward the end. If several adjacent elements need to disappear, delete the whole range once instead of deleting them individually in a loop.
For example, prefer:
items = slices.Delete(items, 100, 200)over one hundred calls that repeatedly remove index 100. Repeated calls make much of the same suffix move again and again.
Second, other slices that share the backing array can observe the rearrangement. Consider this code:
items := []string{"a", "b", "c", "d"}
alias := items
items = slices.Delete(items, 1, 3)
fmt.Println(items)
fmt.Println(alias)items becomes [a d]. alias still has length four, but it refers to the same storage, so it observes the shifted element and the cleared tail slots.
If independent data is required, clone before performing an in-place deletion:
copyOfItems := slices.Clone(items)
copyOfItems = slices.Delete(copyOfItems, 1, 3)The original slice and the clone then have separate outer backing storage.
Vacated tail elements are cleared
After elements shift left, the old tail positions are no longer part of the returned slice. slices.Delete clears those vacated positions to the zero value of the element type.
That detail matters most for slices containing pointers, strings, maps, slices, interfaces, or other values that can keep referenced data reachable. Clearing the unused tail prevents stale references there from remaining solely because of the deletion operation.
You normally do not need to clear those positions yourself. In current Go, tail clearing is part of the slices.Delete contract.
It is still useful to remember this behavior when another alias retains the old length. Such an alias can see zero values in the former tail because the operation changes shared backing storage.
Removing every element preserves nilness
Deleting the full range returns an empty slice:
items := []int{1, 2, 3}
items = slices.Delete(items, 0, len(items))
fmt.Println(len(items))The length is zero. For a non-nil input, the result remains non-nil. A nil input can also be passed with the valid empty range (0, 0), and the result remains nil.
That distinction can matter in code where nil and non-nil empty slices have different serialization or API semantics. Delete does not normalize one form into the other when the result is empty.
Use DeleteFunc when selection is value-based
slices.Delete fits cases where the indexes of the unwanted range are already known. If the rule is based on each element’s value instead, slices.DeleteFunc often states the intent more directly.
For example, removing every disabled record is naturally expressed as a predicate:
records = slices.DeleteFunc(records, func(r Record) bool {
return r.Disabled
})By contrast, if a parser has identified one invalid contiguous span from start to end, slices.Delete(records, start, end) avoids running a predicate across the whole slice.
The choice is about the information already available: use indexes for a known range and a predicate for value-based filtering.
Delete the range you already know
Use slices.Delete when code has a valid contiguous range to remove and preserving element order matters. Assign the returned slice, validate indexes that come from external data, and remember that aliases share the in-place rearrangement.
For multiple neighboring removals, combine them into one range when possible. If removal depends on element properties rather than positions, switch to slices.DeleteFunc instead of forcing index bookkeeping around a predicate.