Replacing part of a slice often starts as an append expression that works but takes a moment to decode. slices.Replace gives that operation a direct name: choose the half-open range s[i:j], provide replacement values, and keep the returned slice.
The replacement doesn’t have to be the same size as the removed range. It can be shorter, longer, or empty, which makes slices.Replace useful for more than one-for-one edits.
Replace a slice range with slices.Replace
The basic call replaces every element from index i up to, but not including, index j:
package main
import (
"fmt"
"slices"
)
func main() {
stages := []string{"draft", "review", "published"}
stages = slices.Replace(stages, 1, 2, "approved", "queued")
fmt.Println(stages)
}The output is:
[draft approved queued published]The range 1:2 contains only "review". Two values take its place, so the result grows from three elements to four.
Assign the return value back to the slice. A replacement can change the slice length and may require a different backing array, so continuing to use only the old slice header can leave the caller with the wrong length or storage.
The replacement range is half-open
slices.Replace(s, i, j, values...) follows normal Go slicing rules. Index i is included and j is excluded.
For this slice:
nums := []int{10, 20, 30, 40, 50}
nums = slices.Replace(nums, 1, 4, 7, 8)
fmt.Println(nums)The removed range is [20 30 40], producing:
[10 7 8 50]An off-by-one error here can silently replace a neighboring value. Reading i:j as the exact range being removed is a useful check before supplying the replacement values.
The indexes must also describe a valid slice range. Calls with i > j, a negative index, or an upper bound beyond len(s) panic. If indexes come from external input, validate them before calling slices.Replace rather than treating a panic as normal control flow.
Use fewer or more replacement values
The number of replacement values controls the new length. There is no requirement that it match j-i.
Replacing three values with one shrinks the slice:
parts := []string{"api", "v1", "users", "active", "42"}
parts = slices.Replace(parts, 1, 4, "accounts")
fmt.Println(parts)Result:
[api accounts 42]Replacing one value with several grows it:
flags := []string{"start", "end"}
flags = slices.Replace(flags, 1, 2, "check", "write", "end")
fmt.Println(flags)Result:
[start check write end]That flexibility is useful when a parser expands one token into several values or a normalization pass collapses several entries into one canonical value.
Remove a range by providing no values
An empty replacement removes the selected range:
stages := []string{"draft", "review", "queued", "published"}
stages = slices.Replace(stages, 1, 3)
fmt.Println(stages)The result is:
[draft published]For code whose intent is specifically deletion, slices.Delete(stages, 1, 3) communicates that intent more directly. slices.Replace is most useful when deletion is one case of a broader replacement operation, or when replacement values are already being passed variadically.
When a replacement shortens the slice, the standard-library operation clears elements in the removed tail region before returning. That behavior helps avoid retaining references through obsolete slice slots, but it doesn’t detach every alias that may point at the same backing array.
Insert values with an empty range
When i and j are equal, the selected range contains no elements. Replacement values are inserted at that position:
stages := []string{"draft", "published"}
stages = slices.Replace(stages, 1, 1, "review")
fmt.Println(stages)Result:
[draft review published]slices.Insert is clearer when insertion is the sole operation. Still, the empty-range behavior matters when i and j are computed and the same code path handles insertion, replacement, and removal.
For example, an editor can represent a change as a start index, an end index, and zero or more new values. Passing that edit directly to slices.Replace keeps all three cases under the same range model.
Remember that backing storage may be shared
slices.Replace modifies the supplied slice’s storage when it can. That means another slice sharing the same backing array may observe changes.
base := []string{"a", "b", "c", "d"}
view := base[:3]
view = slices.Replace(view, 1, 2, "x")
fmt.Println(view)
fmt.Println(base)A caller should not assume that base remains independent just because view has its own slice header. With a same-length replacement such as this one, the existing backing array can be reused and the shared element changes.
If the original data must stay isolated, clone before editing:
copyOfBase := slices.Clone(base)
copyOfBase = slices.Replace(copyOfBase, 1, 2, "x")Cloning makes the ownership decision explicit. It also avoids depending on whether a particular replacement happens to fit in the existing capacity.
Don’t depend on allocation behavior
A growing replacement may fit in the current capacity or may allocate new storage. Code should work correctly in either case.
This is another reason to use the returned slice and avoid making correctness depend on backing-array identity. Capacity is an implementation detail of the current slice value, not a promise that slices.Replace will preserve aliases after every edit.
If stable storage identity is part of an API contract, slices.Replace is usually the wrong abstraction. A fixed-size buffer or an explicit copy strategy makes that requirement clearer.
Pick the operation that matches the edit
Use slices.Replace when the core operation is replacing a known index range and the number of incoming values may differ from the number removed. It naturally covers shrinking, growing, insertion through an empty range, and removal through an empty replacement.
Keep two details at the call site: assign the returned slice, and treat the indexes exactly like a normal s[i:j] expression. If the operation is purely insertion or deletion, slices.Insert or slices.Delete can state the intent more precisely. For a general range edit, slices.Replace keeps the code compact without hiding how the slice changes.