Replacing part of a Go slice sounds simple until the replacement has a different length from the range it replaces. A two-element range might become one element, four elements, or nothing at all. At that point, a few append calls can work, but they make the indexing and storage behavior harder to read than the operation itself.
slices.Replace handles that splice-like operation directly. Give it a slice, a half-open range, and the replacement values; it returns the resulting slice. The function can shrink, preserve, or grow the length depending on how many values you provide.
What slices.Replace does
The function has this shape:
func Replace[S ~[]E, E any](s S, i, j int, v ...E) SIt replaces s[i:j] with v. As with ordinary Go slicing, i is inclusive and j is exclusive.
Suppose a processing pipeline no longer needs separate validate and save stages. They can be replaced with one check stage:
package main
import (
"fmt"
"slices"
)
func main() {
stages := []string{"parse", "validate", "save", "notify"}
stages = slices.Replace(stages, 1, 3, "check")
fmt.Println(stages)
}The output is:
[parse check notify]The range 1:3 covers validate and save, so both are removed before check occupies their place.
Assign the return value. slices.Replace may change the slice length and may need a different backing array when the result grows beyond available capacity. Ignoring the returned slice means keeping the old slice header even though the operation may have modified its storage.
The replacement can be shorter, equal, or longer
Thinking about slices.Replace as a fixed-size overwrite misses most of its value. The number of replacement values doesn’t have to match j-i.
Replacing two elements with one shrinks the slice:
values := []int{10, 20, 30, 40}
values = slices.Replace(values, 1, 3, 99)
fmt.Println(values) // [10 99 40]Replacing two elements with two keeps the same length:
values = []int{10, 20, 30, 40}
values = slices.Replace(values, 1, 3, 21, 31)
fmt.Println(values) // [10 21 31 40]A longer replacement grows the slice:
values = []int{10, 20, 30, 40}
values = slices.Replace(values, 1, 2, 21, 22, 23)
fmt.Println(values) // [10 21 22 23 30 40]That makes the helper useful for more than editing one field in a sequence. It can express a small structural rewrite while keeping the range being changed explicit.
Use an empty range to insert values
A range where i == j contains no elements. Replacing that empty range therefore inserts the new values at that position.
numbers := []int{1, 2, 5}
numbers = slices.Replace(numbers, 2, 2, 3, 4)
fmt.Println(numbers) // [1 2 3 4 5]This is valid at the end of the slice too:
numbers = slices.Replace(numbers, len(numbers), len(numbers), 6)For code whose intent is plainly insertion, slices.Insert may communicate that intent better. Replace is especially handy when one piece of code already thinks in terms of a range and the replacement may contain zero, one, or several values.
The reverse works as well. Passing no replacement values removes the selected range:
numbers := []int{1, 2, 3, 4, 5}
numbers = slices.Replace(numbers, 1, 4)
fmt.Println(numbers) // [1 5]If the operation is purely deletion, slices.Delete is usually the clearer name. The point isn’t to use Replace for every possible splice; it’s to choose the helper that makes the intended operation easiest to recognize.
slices.Replace modifies slice storage
slices.Replace modifies the supplied slice rather than promising a fresh independent copy. When existing capacity is sufficient, the result can continue using the same backing array. When growth requires more space, an allocation may produce a new backing array.
That distinction matters when another slice aliases the same storage:
items := []string{"a", "b", "c", "d"}
alias := items
items = slices.Replace(items, 1, 3, "x")
fmt.Println(items) // [a x d]
fmt.Println(alias[1]) // xalias isn’t a snapshot of the original values. A replacement can move or overwrite elements in the shared backing array, so code holding an alias may observe those changes.
If the original sequence must remain unchanged, clone it before replacing:
updated := slices.Clone(items)
updated = slices.Replace(updated, 1, 2, "new")This makes the ownership boundary explicit. It’s often more useful to decide whether mutation is acceptable first and only then choose the slice helper.
Growth can change the backing array
Capacity affects what happens when a replacement makes the result longer. Consider a slice with no spare capacity:
values := []int{1, 2, 3}
values = slices.Replace(values, 1, 2, 20, 21, 22)The new result needs five elements. If the old backing array can’t hold them, Replace has to return a slice backed by enough storage for the larger result.
Don’t write code that depends on a particular capacity after replacement. Capacity growth is an implementation concern, not the contract you should build application logic around. What matters is that the returned slice contains the requested sequence.
This is another reason the assignment is mandatory:
values = slices.Replace(values, 1, 2, 20, 21, 22)The returned slice header carries the correct length and, when needed, the new backing array.
Shrinking replacements zero the vacated tail
When the replacement is shorter than s[i:j], the resulting slice has fewer elements. On current Go versions, slices.Replace zeroes the elements between the new length and the old length.
This behavior matters for slices whose elements can keep other data reachable, such as pointers, maps, slices, strings, or structs containing them. A shrinking replacement shouldn’t leave stale references sitting in the vacated tail of the old logical slice.
Go 1.22 standardized this zeroing behavior for the shrinking helpers in slices, including Replace. If you’re reasoning about code that must preserve behavior on an older Go toolchain, check that version’s documentation rather than assuming the current guarantee.
You generally shouldn’t reslice beyond the returned length just to inspect those zeroed slots. The useful consequence is simpler: on supported current versions, you don’t need a separate cleanup loop solely to clear the tail after a shrinking slices.Replace call.
Invalid ranges panic
The indices follow normal slice-range rules. slices.Replace panics when s[i:j] isn’t a valid slice of s, including cases such as i > j, a negative index, or j > len(s).
That makes it a good fit when the indices come from trusted program logic. If the range originates from a request, file, database row, or another untrusted boundary, validate it before calling Replace.
A compact validation looks like this:
if i < 0 || j < i || j > len(values) {
return fmt.Errorf("invalid replacement range [%d:%d] for length %d", i, j, len(values))
}
values = slices.Replace(values, i, j, replacement...)Turning malformed external input into a normal error is usually preferable to letting it become a process-level panic.
There’s one subtle boundary worth remembering: i == j == len(s) is valid because it describes an empty range at the end. That’s what allows insertion after the final existing element.
Empty and nil slices have useful boundary behavior
An empty slice still has one valid empty range: 0:0. You can insert into it with Replace:
var values []int
values = slices.Replace(values, 0, 0, 10, 20)
fmt.Println(values) // [10 20]If there are no replacement values either, a nil input remains nil:
var values []int
values = slices.Replace(values, 0, 0)
fmt.Println(values == nil) // trueMost code should still use len(values) == 0 when it only cares whether the slice is empty. Nilness becomes relevant at boundaries where your program deliberately distinguishes nil from a non-nil empty slice.
Prefer Replace when the operation is a range rewrite
A hand-written splice often looks something like append(s[:i], ...), followed by more append logic for the suffix. That can be perfectly valid, but readers have to reconstruct which range disappears, where the replacement lands, and whether overlapping storage is safe.
slices.Replace states the operation in one place: this half-open range becomes these values. It’s a good choice for token streams, ordered configuration entries, processing stages, argument lists, and similar sequences where a contiguous section needs to be rewritten.
Use slices.Insert when nothing is being removed, slices.Delete when nothing is being inserted, and slices.DeleteFunc when deletion depends on a predicate. Reach for slices.Replace when the real operation is replacing a known range, especially when the old and new ranges can have different lengths.
The practical rule is simple: validate externally supplied indices, assign the returned slice, and treat the input’s backing storage as mutable. With those constraints clear, slices.Replace turns an otherwise fiddly slice splice into code whose intent is visible at the call site.