Adding a value to the middle of a Go slice takes more work than appending at the end. Elements after the insertion point need to move, and the operation may require a larger backing array. slices.Insert packages that splice into one standard-library call.
The function accepts a slice, an index, and one or more values. It returns the resulting slice, so the usual form assigns that result back to the slice variable.
Insert one value with slices.Insert
Suppose a processing pipeline has three stages and a validation stage needs to run before storage:
package main
import (
"fmt"
"slices"
)
func main() {
stages := []string{"parse", "store", "notify"}
stages = slices.Insert(stages, 1, "validate")
fmt.Println(stages)
}The result is:
[parse validate store notify]Index 1 identifies the position where the new value begins. The value previously at that position, along with everything after it, moves toward the end.
This index follows normal slice boundaries. Index 0 inserts at the front, while len(stages) inserts at the end.
Insert several values in one call
The final parameter is variadic, so one call can insert multiple values while preserving their argument order:
numbers := []int{10, 40, 50}
numbers = slices.Insert(numbers, 1, 20, 30)
fmt.Println(numbers)The resulting slice is:
[10 20 30 40 50]This is clearer than making two separate insertions when both values belong at the same position. A single operation also avoids shifting the trailing elements once per inserted group.
A source slice can supply the values with ...:
middle := []int{20, 30}
numbers = slices.Insert([]int{10, 40, 50}, 1, middle...)That form is useful when the inserted block already exists as a slice.
Keep the returned slice
slices.Insert modifies slice storage as part of its operation, but callers still need to use the returned slice. An insertion can increase the length beyond the capacity available in the original backing array. When that happens, the result needs different storage.
Write this:
items = slices.Insert(items, 2, newItem)rather than discarding the return value:
slices.Insert(items, 2, newItem)Even when the original capacity happens to be sufficient, relying on the old slice header gives the wrong length. Treat the returned value as the authoritative slice after the insertion.
This also means aliases deserve care. If another slice shares the same backing array, an insertion that reuses capacity can change elements visible through that alias. If independent storage is required, clone first:
copyOfItems := slices.Clone(items)
copyOfItems = slices.Insert(copyOfItems, 2, newItem)The clone separates the top-level slice storage before the splice.
Handle insertion indexes carefully
Valid insertion indexes range from 0 through len(s), inclusive. Passing an index greater than the slice length causes a panic.
For a slice with three elements, these positions are valid:
index: 0 1 2 3
| | | |
values: A B CThe last position is useful for code that computes a position and permits an end insertion:
values = slices.Insert(values, len(values), extra)If the index comes from external input or arithmetic that isn’t already constrained by the program, validate it before calling slices.Insert. A panic is appropriate for a broken internal invariant, but it usually isn’t a good response to an ordinary invalid request.
Middle insertion still has a movement cost
slices.Insert makes the code concise; it doesn’t turn middle insertion into a constant-cost operation. Elements at and after the insertion point must make room for the new values. The standard-library contract describes the operation as O(len(s) + len(v)).
For occasional edits to a modest ordered collection, that cost is often a reasonable trade-off for simple slice-based code. A workload that repeatedly inserts near the front of a large slice can spend substantial time moving elements. In that case, reconsider the representation or batch the changes when possible.
Appending at the end has different characteristics. If the operation is always an end insertion, ordinary append is more familiar:
items = append(items, newItem)Use slices.Insert when the position itself is part of the operation.
Pair insertion with binary search for sorted slices
A common use case is maintaining a sorted slice. slices.BinarySearch returns an insertion position when a value isn’t present, and that position can feed directly into slices.Insert:
func addSorted(numbers []int, value int) []int {
index, found := slices.BinarySearch(numbers, value)
if found {
return numbers
}
return slices.Insert(numbers, index, value)
}This keeps the slice sorted without sorting the entire collection after every addition. The insertion itself can still shift trailing elements, so the pattern fits occasional additions better than insertion-heavy workloads.
If duplicate values are allowed, decide where a new duplicate belongs. BinarySearch reports an index at which the target is found, but code that needs a specific first-or-last duplicate position needs a more explicit boundary rule.
Choose the operation that matches the edit
Several slice helpers can look similar around a splice. slices.Insert adds values without intentionally removing a range. slices.Replace substitutes a selected range with zero or more values. slices.Delete removes a known range.
Keeping those intents separate makes later edits easier to inspect. A call to slices.Insert(records, i, record) says immediately that existing records should remain and a new one should occupy position i.
For an insertion, retain the returned slice, keep the index within 0 through len(s), and remember that shared backing storage can make aliases observe changes. Those details matter more than the compact syntax, especially once the slice is passed across function boundaries.