slices.Replace changes a contiguous range of a slice and can make the resulting slice shorter, longer, or the same length. That makes it more than element assignment: the operation combines range removal and insertion while retaining Go slice storage semantics.

Its signature accepts any slice element type:

func Replace[S ~[]E, E any](s S, i, j int, v ...E) S

The half-open range s[i:j] is replaced by the values in v. Since the resulting length can change, the returned slice value is part of the operation’s contract.

Replacement size controls the resulting length

Replacing two elements with three grows the logical slice by one:

package main

import (
    "fmt"
    "slices"
)

func main() {
    names := []string{"api", "worker", "cache", "db"}
    names = slices.Replace(names, 1, 3, "queue", "index", "proxy")

    fmt.Println(names)
}

The result is:

[api queue index proxy db]

The original range contains worker and cache. The replacement occupies that position, and the suffix beginning with db follows it.

A shorter replacement reduces the length:

values := []int{10, 20, 30, 40, 50}
values = slices.Replace(values, 1, 4, 99)

fmt.Println(values) // [10 99 50]

An empty replacement removes the selected range, while an empty selected range acts as insertion:

values = slices.Replace(values, 1, 1, 7, 8)

These cases share one range-replacement model rather than requiring separate indexing code.

The returned slice must replace the old header

A slice value contains a pointer, length, and capacity. Replace may alter the length and may need a different backing array when existing capacity cannot hold a larger result.

Code should therefore retain the returned value:

items := []string{"a", "b", "c"}
items = slices.Replace(items, 1, 2, "x", "y")

Ignoring the return value leaves items with its previous slice header. Even when some backing storage was modified, that old header does not describe the complete replacement result.

The same rule applies when the operation shrinks the slice. The returned header carries the new logical length.

Existing capacity can permit storage reuse

Replace modifies the supplied slice in place when the operation can fit the result into available storage. A larger replacement can instead require allocation.

That distinction means callers should not treat the returned slice as an independent copy of the input. Another slice that aliases the same backing array can observe mutations when storage is reused.

base := make([]int, 3, 6)
copy(base, []int{1, 2, 3})
alias := base

base = slices.Replace(base, 1, 2, 7, 8)

fmt.Println(base)  // [1 7 8 3]
fmt.Println(alias) // shared storage may expose changed elements

The alias still has its own length of three, but its visible elements come from backing storage that the replacement operation can modify.

When an unchanged source must remain available, cloning establishes separate outer storage before replacement:

updated := slices.Clone(base)
updated = slices.Replace(updated, 1, 2, 7, 8)

The clone is shallow, so reference-bearing elements inside the slice can still refer to shared nested state.

Shorter results clear obsolete elements

When the replacement is shorter than s[i:j], the standard library zeroes elements between the new length and the original length. This behavior matters for slices containing pointers, maps, slices, interfaces, or other values that can retain references.

Consider a pointer slice:

type Node struct {
    ID int
}

nodes := []*Node{
    {ID: 1},
    {ID: 2},
    {ID: 3},
    {ID: 4},
}

nodes = slices.Replace(nodes, 1, 3, &Node{ID: 9})

fmt.Println(len(nodes)) // 3

The obsolete tail slot from the longer original slice is cleared. Removed references are not left solely in stale tail positions by this operation.

This detail differs from hand-written slice manipulation that merely shortens a header without clearing storage. The visible result can look identical while the unreachable portion of the old logical slice retains different values.

Bounds follow ordinary slice rules

The selected range must be valid for s. Replace panics when j exceeds len(s) or when s[i:j] is not a valid slice expression.

values := []int{1, 2, 3}
_ = slices.Replace(values, 2, 4, 9) // panic

For indices derived from external input, validation belongs at the boundary where those indices enter the program if panic is not an acceptable failure mode.

The valid empty range at the end of a slice can append values:

values := []int{1, 2}
values = slices.Replace(values, len(values), len(values), 3, 4)

fmt.Println(values) // [1 2 3 4]

The operation still follows the same half-open range rules used by ordinary slicing.

Replacement preserves named slice types

The type parameter S ~[]E allows named slice types and returns the same slice type:

type Bytes []byte

packet := Bytes{0x01, 0x02, 0x03}
packet = slices.Replace(packet, 1, 2, 0xAA, 0xBB)

fmt.Printf("%T %v\n", packet, packet)
// main.Bytes [1 170 187 3]

No conversion back from []byte is required. The range operation remains expressed in terms of the named type used by the surrounding API.

slices.Replace is most precise when a contiguous range is the unit being changed. Its key boundary is storage identity: it returns the correct new slice header, but it does not promise isolation from aliases or deep copies of reference-bearing elements. Code that requires either property has to establish it separately.