Removing a known range from a Go slice is easy to express with slices.Delete. Give it the slice and a half-open index range, and it closes the gap for you.
The small details matter. slices.Delete changes the slice’s contents, returns a slice with a new length, and panics for an invalid range. Using it well means treating those behaviors as part of the operation rather than as implementation trivia.
Remove a range with slices.Delete
Suppose a pipeline contains stages that are no longer needed:
package main
import (
"fmt"
"slices"
)
func main() {
stages := []string{"parse", "validate", "store", "notify"}
stages = slices.Delete(stages, 1, 3)
fmt.Println(stages)
}The output is:
[parse notify]The call removes stages[1:3]. Index 1 is included and index 3 is excluded, matching Go’s normal slice syntax. The values at indexes 1 and 2 disappear, while the value that was at index 3 remains.
That half-open convention makes range calculations consistent with expressions such as stages[:i], stages[i:j], and stages[j:].
Always use the returned slice
A deletion changes the logical length of the collection. Assign the result back:
items = slices.Delete(items, start, end)Calling the function and discarding its result is a bug in normal use:
slices.Delete(items, start, end)The function can move elements inside the existing backing array, but the caller’s slice variable still has its old length until it receives the returned slice header. The return value is therefore part of the operation, not an optional convenience.
This is the same general habit used with append: when an operation returns the slice that represents the new collection, keep that result.
The indexes describe a half-open range
For a slice with length 5, these calls have distinct effects:
values = slices.Delete(values, 0, 1) // remove the first element
values = slices.Delete(values, 2, 4) // remove indexes 2 and 3
values = slices.Delete(values, 3, 3) // remove nothingA range where both indexes are equal is valid and removes zero elements. That can simplify code that calculates a range dynamically because an empty range doesn’t need a special branch.
Invalid ranges still panic. For example, an end index beyond len(values), a negative index, or a start index greater than the end index does not describe a valid slice range.
If indexes come from external input, validate them before calling slices.Delete. A panic is appropriate for a violated internal invariant in some programs, but it is usually the wrong response to malformed user data or an invalid API request.
Deletion reuses the slice storage
slices.Delete modifies the supplied slice in place. It shifts the suffix after the removed range toward the front and returns the shortened slice.
Consider:
records := []string{"a", "b", "c", "d", "e"}
records = slices.Delete(records, 1, 4)
fmt.Println(records)The result is:
[a e]This doesn’t promise a fresh backing array. Code that requires an independent copy should make that requirement explicit, for example by cloning before deletion:
copyOfRecords := slices.Clone(records)
copyOfRecords = slices.Delete(copyOfRecords, 1, 3)Now changes made through copyOfRecords won’t rewrite elements in the original slice’s backing array.
The clone is shallow. If the elements themselves contain pointers, slices, maps, or other reference-bearing values, those referenced objects can still be shared.
Removed slots are cleared
Current slices.Delete clears the trailing slots that fall outside the new length after elements have been shifted. This matters for slices containing pointers or other values that can retain references.
You generally don’t need to add a manual clearing loop after slices.Delete. The standard helper already handles the vacated tail.
The slice can still retain its previous capacity. Deleting elements shortens the length; it does not promise to reduce capacity. If retaining extra capacity is undesirable at a specific boundary, slices.Clip can restrict the returned slice’s capacity to its length:
items = slices.Delete(items, start, end)
items = slices.Clip(items)Don’t add Clip automatically after every deletion. Spare capacity can be useful when more values will be appended soon, and clipping may cause a later append to allocate.
Delete one contiguous range instead of many individual elements
The cost of deletion includes shifting the suffix after the removed range. Repeating single-element deletions can shift much of the same data again and again.
If the indexes to remove form one contiguous block, delete that block in one call:
items = slices.Delete(items, first, last+1)This also makes the intent clearer than a loop that repeatedly deletes at the same position.
When removal depends on a predicate rather than a known range, slices.DeleteFunc is the better match:
items = slices.DeleteFunc(items, func(item Item) bool {
return item.Expired
})Use Delete when the positions are already known. Use DeleteFunc when each element must be inspected to decide whether it stays.
Be careful when indexes were computed before earlier edits
Slice indexes are positional. Once a deletion happens, later elements move toward the front.
Suppose you plan to remove original indexes 2 and 5 with two calls. After removing index 2, the element that was at index 5 is now at index 4. Reusing the old index can remove the wrong element or produce an invalid range.
For several non-contiguous known indexes, common options are to process deletions from the highest index toward the lowest, build a filtered result in one pass, or use DeleteFunc when the decision can be represented as a predicate.
The right choice depends on what your indexes mean. If they identify positions in the original snapshot, deleting from the end avoids invalidating earlier positions. If they are recalculated after each edit, forward processing can still be correct.
Empty results preserve nilness
Deleting the complete contents of a non-nil slice produces an empty non-nil slice. Applying an empty deletion to a nil slice preserves nilness:
var values []int
values = slices.Delete(values, 0, 0)
fmt.Println(values == nil) // trueMost application code can treat nil and empty slices similarly, but serialization, API contracts, and tests sometimes distinguish them. If that distinction matters, make it part of the expected behavior instead of relying on assumptions about allocation.
Use slices.Delete when positions are the real input
slices.Delete fits operations where the program already knows the contiguous range to remove: dropping selected columns from an intermediate row, removing a span of tokens, cutting obsolete steps from an ordered workflow, or applying an editor-style range deletion.
Keep the returned slice, validate untrusted indexes, and remember that later positions move after an edit. If removal is based on element properties rather than positions, switch to slices.DeleteFunc instead. That keeps the code aligned with the actual decision being made.