The slices Package

A comprehensive guide to the slices package in Go - covering functions for searching, comparing, modifying, and sorting slices with practical examples

The slices package, added to the standard library in Go 1.21, collects the most common operations developers perform on slices into a single, generic API. Before this package existed, every project carried its own set of helper functions for things like checking if a value was in a slice, removing elements, or finding minimum values. Those helpers were often written differently, tested inconsistently, and had subtle bugs around nil slices or capacity management. The slices package eliminates that duplication and gives everyone a well-tested, consistent toolbox.

A slice in Go is a view into an underlying array, described by a pointer, a length, and a capacity. Because of that internal structure, operations like deleting an element or compacting duplicates need to handle not just the data but also the length and capacity correctly. The slices package handles those details for you, including zeroing out reclaimed memory to avoid leaks, preserving nil-ness when appropriate, and panicking with clear error messages when preconditions aren't met.

All functions in this package are generic, meaning they work on slices of any type without reflection or type assertions. Some functions require the element type to be comparable, which is the set of types Go can compare with == and !=. For types that aren't comparable (like slices, maps, or functions), the package provides Func variants that accept a custom comparison or equality function.

The functions covered in this document are the ones you'll reach for most often in production code: Contains and Index, Equal and Compare, BinarySearch, Delete and Compact, Insert and Replace, Clone and Clip, Sort and IsSorted, and Min/Max.

Checking Whether a Slice Contains an Element

Contains returns true if a given value exists anywhere in the slice, and ContainsFunc returns true if at least one element satisfies a predicate you provide. These replace the manual loop that almost every Go codebase had.

package main
import (
	"fmt"
	"slices"
)
func main() {
	names := []string{"Alice", "Bob", "Charlie"}
	fmt.Println(slices.Contains(names, "Bob"))     // true
	fmt.Println(slices.Contains(names, "Dave"))    // false
	numbers := []int{3, 1, 4, 1, 5}
	hasNegative := slices.ContainsFunc(numbers, func(n int) bool {
		return n < 0
	})
	fmt.Println(hasNegative) // false
}

Contains uses == under the hood, so the element type must be comparable. If you try to use Contains on a slice of slices, it won't compile. ContainsFunc works around that by letting you define what "equal" means.

Nil slices and empty slices:

Both Contains and ContainsFunc handle nil and empty slices correctly — they simply return false since there are no elements to match.

When you need the position of an element, not just its existence, use Index. It returns the first index where the value is found, or -1 if it isn't present. IndexFunc does the same with a predicate.

s := []string{"apple", "banana", "cherry", "banana"}
fmt.Println(slices.Index(s, "banana")) // 1
fmt.Println(slices.Index(s, "grape"))  // -1
pos := slices.IndexFunc(s, func(fruit string) bool {
	return len(fruit) > 5
})
fmt.Println(pos) // 2 (cherry)

The key difference between these and BinarySearch is that Index performs a linear scan from the beginning. For unsorted data, this is what you need.

Index scans the entire slice:

If your slice has 100,000 elements and the value is near the end, Index will walk through all of them. If you have sorted data, BinarySearch is dramatically faster. Don't use Index for a membership check inside a hot loop on sorted data.

Searching in Sorted Slices

BinarySearch finds the index of a target in a slice that is sorted in ascending order. It returns the index and a boolean indicating whether the target was actually found. If the target isn't present, the index tells you where it would be inserted to maintain order.

sorted := []int{1, 3, 5, 7, 9}
idx, found := slices.BinarySearch(sorted, 5)
fmt.Println(idx, found) // 2 true
idx, found = slices.BinarySearch(sorted, 6)
fmt.Println(idx, found) // 3 false (6 would go at index 3)

The slice must be sorted before calling BinarySearch. If it isn't, the function still runs without panicking, but the result is meaningless — it might return false even though the element exists, or report an index that doesn't make sense.

Unsorted input gives undefined results:

BinarySearch does not check whether the input is sorted. Passing an unsorted slice produces unpredictable output, not a compile error or a runtime panic. If you're not certain your data is sorted, sort it first or use Index.

BinarySearchFunc works the same way but uses a custom comparison function instead of < and >. The comparison function receives a slice element and the target, and returns a negative number if the element should come before the target, a positive number if it should come after, and 0 if they match. This is useful when you need to search by a field of a struct or use a case-insensitive match.

type Person struct {
	Name string
	Age  int
}
people := []Person{
	{"Alice", 30},
	{"Bob", 25},
	{"Charlie", 35},
}
// Search by Age, assuming slice is sorted by Age.
idx, found := slices.BinarySearchFunc(people, 25, func(p Person, targetAge int) int {
	return p.Age - targetAge
})
fmt.Println(idx, found) // 0 true

Comparing Two Slices

Equality between slices is a common need that Go doesn't provide directly — you can't use == on two slices. Equal fills that gap for comparable element types, and EqualFunc does it with a custom predicate.

a := []int{1, 2, 3}
b := []int{1, 2, 3}
c := []int{1, 2, 4}
fmt.Println(slices.Equal(a, b)) // true
fmt.Println(slices.Equal(a, c)) // false

Two slices are equal if they have the same length and every element at each index is equal. Nil slices and empty slices are considered equal — an important detail that prevents subtle bugs when comparing return values from functions.

EqualFunc shines when comparing slices of structs or when you need a relaxed notion of equality. The provided function receives two elements (one from each slice at the same index) and returns true if they should be treated as equal.

type user struct {
	name string
	age  int
}
u1 := []user{{"Alice", 30}, {"Bob", 25}}
u2 := []user{{"alice", 30}, {"bob", 25}}
eq := slices.EqualFunc(u1, u2, func(a, b user) bool {
	return strings.EqualFold(a.name, b.name) && a.age == b.age
})
fmt.Println(eq) // true

Equal stops at the first mismatch:

Equal does not compare the entire slice if it can detect a difference early. It returns false as soon as it finds a mismatched element or a length difference. This is efficient but means you can't use it to collect all differences — it answers "are they equal?" not "where do they differ?"

For ordering comparisons (less than, greater than, equal), the package offers Compare and CompareFunc. They behave like the cmp.Compare function: they return -1 if the first slice is "less," 0 if equal, and +1 if the first is "greater." Comparison is lexicographic — elements are compared pair by pair, and the first differing pair determines the result. If one slice is a prefix of the other, the shorter slice is considered less.

fmt.Println(slices.Compare([]int{1, 2}, []int{1, 2, 3})) // -1
fmt.Println(slices.Compare([]int{2, 1}, []int{1, 2}))    // 1
fmt.Println(slices.Compare([]int{1, 2}, []int{1, 2}))    // 0

Removing Elements

Delete removes a contiguous range of elements between indices i (inclusive) and j (exclusive). It returns the modified slice, which may have a smaller length. The elements that were shifted out are zeroed in memory, so the garbage collector can reclaim any objects they referenced.

s := []string{"a", "b", "c", "d", "e"}
s = slices.Delete(s, 1, 3)
fmt.Println(s) // [a d e]

The indices follow the same half-open interval convention as slice expressions: i is the first element to remove, j is the index after the last element to remove. Delete panics if j is greater than the slice length or if i > j.

Delete preserves capacity:

Delete does not shrink the underlying array. The length changes, but the capacity stays the same. If you delete many elements and want to release the unused memory, follow the call with slices.Clip(s).

DeleteFunc removes all elements for which a predicate returns true. It returns a slice containing only the elements that didn't match.

nums := []int{1, -2, 3, -4, 5}
nums = slices.DeleteFunc(nums, func(n int) bool {
	return n < 0
})
fmt.Println(nums) // [1 3 5]

This is more convenient than a loop that manually builds a new slice, and it correctly zeroes the trailing elements.

Compact is for a different cleanup: it removes consecutive duplicate elements, keeping only the first occurrence of each run. It modifies the slice in place, reducing the length. The remaining capacity is untouched, and elements between the new length and the old capacity are zeroed.

data := []int{1, 1, 2, 3, 3, 3, 4}
data = slices.Compact(data)
fmt.Println(data) // [1 2 3 4]

CompactFunc does the same with a custom equality function, which must return true if two adjacent elements should be considered equal.

Compact only removes consecutive duplicates:

If your slice contains [1, 2, 1], Compact will leave it unchanged because the two 1s are not next to each other. To remove all duplicates regardless of position, you must sort the slice first, then compact, or use a different approach entirely. Calling Compact on an unsorted slice and expecting all duplicates to vanish is a common mistake.

Copying and Controlling Capacity

Clone creates a shallow copy of a slice. The returned slice has its own backing array, so modifying elements in the clone won't affect the original, and vice versa. The elements themselves are copied by assignment, so if the elements are pointers or reference types (like maps or slices), the underlying data is shared.

original := []int{10, 20, 30}
copied := slices.Clone(original)
copied[0] = 99
fmt.Println(original) // [10 20 30]
fmt.Println(copied)   // [99 20 30]

Clone is particularly useful when extracting a subslice that you intend to modify. Slicing an existing slice with original[1:4] creates a new slice header but points to the same backing array. Mutating the subslice mutates the original. Wrapping the subslice expression in slices.Clone eliminates that shared state.

orig := []int{1, 2, 3, 4, 5}
sub := slices.Clone(orig[1:4])
sub[0] = 99
fmt.Println(orig) // [1 2 3 4 5] — unchanged
fmt.Println(sub)  // [99 3 4]

Clip removes unused capacity by setting the capacity equal to the length. This can be important for memory management when a slice was built up with many appends and then reduced in size. The backing array may have a large capacity that keeps memory pinned. Clip forces Go to allocate a new, smaller backing array if the capacity exceeds the length, or adjusts the slice header if the capacity already equals the length.

s := make([]int, 0, 1000)
s = append(s, 1, 2, 3)
s = slices.Clip(s)
fmt.Println(len(s), cap(s)) // 3 3

Grow increases the capacity of a slice to guarantee room for at least n more elements, avoiding repeated allocations during a series of append calls. It's a performance hint, similar to pre-sizing with make, but can be applied to an existing slice.

s := []int{1, 2}
s = slices.Grow(s, 10)
fmt.Println(cap(s)) // at least 12 (existing length 2 + 10)

Memory pressure relief:

If you've just filtered a large slice down to a few elements and that slice will live for a long time, chaining DeleteFunc (or Compact) with Clip ensures you aren't holding onto a massive backing array you no longer need. This is a production pattern worth remembering.

Inserting and Replacing Elements

Insert adds one or more values at a specific index, shifting the existing elements to the right and growing the slice as needed.

s := []string{"Alice", "Charlie", "Diana"}
s = slices.Insert(s, 1, "Bob")
fmt.Println(s) // [Alice Bob Charlie Diana]

You can insert multiple values by providing them as variadic arguments:

s = slices.Insert(s, 2, "Eve", "Frank")

Insert panics if the index is out of range (less than 0 or greater than the length). It is efficient for small numbers of insertions but linear in the number of elements after the insertion point, so inserting into the front of a large slice repeatedly is costly.

Replace overwrites a range of elements with a new set of values. The new values can be fewer, more, or the same number as the range being replaced. This means you can use Replace to delete and insert in a single operation.

nums := []int{1, 2, 3, 4, 5}
nums = slices.Replace(nums, 1, 4, 8, 9)
fmt.Println(nums) // [1 8 9 5]

Here, elements at indices 1, 2, and 3 are replaced by 8 and 9, effectively shortening the slice by one element. You can also pass zero new values to delete the range, or more values than the range to grow the slice.

nums = slices.Replace(nums, 0, 0, 0) // insert 0 at the front

Replace with i == j:

Setting i equal to j results in an insertion without removing any elements. Setting v to no values results in a pure deletion. This makes Replace a flexible Swiss Army knife for in-place modifications, but be explicit about your intent — Insert and Delete are more readable for simple cases.

Sorting and Checking Order

Sort arranges a slice of any ordered type in ascending order. The element type must implement cmp.Ordered, which covers all integer types, floating-point types, and strings. The sort is not guaranteed to be stable (equal elements may change their relative order).

words := []string{"zebra", "apple", "mango"}
slices.Sort(words)
fmt.Println(words) // [apple mango zebra]

SortFunc allows sorting by a custom comparison function that returns a negative number if a should come before b, a positive number if a should come after, and 0 if they are equivalent. This is how you sort structs by a specific field or implement case-insensitive sorting.

people := []struct{ Name string; Age int }{
	{"Charlie", 35},
	{"Alice", 30},
	{"Bob", 25},
}
slices.SortFunc(people, func(a, b struct{ Name string; Age int }) int {
	return a.Age - b.Age
})
// Result sorted by Age: Bob (25), Alice (30), Charlie (35)

SortStableFunc is the same as SortFunc but guarantees that elements that compare equal retain their original order. This is important when sorting by multiple criteria in multiple passes — the stable sort preserves the ordering established by previous sorts.

To check whether a slice is already sorted, use IsSorted (for ordered types) or IsSortedFunc (with a custom comparator). Both return a simple boolean.

fmt.Println(slices.IsSorted([]int{1, 2, 3})) // true
fmt.Println(slices.IsSorted([]int{3, 1, 2})) // false

Sorting floats and NaNs:

Sort on a slice of float64 that contains NaN will behave unexpectedly because NaN is not ordered. Comparisons involving NaN always return false, which violates the sorting contract. The sort may complete without panicking but the resulting order is arbitrary. The sort package's Float64s function has the same limitation, but it's something to watch for when using slices.Sort with floats.

Finding Minimum and Maximum Values

Min and Max return the smallest and largest element in a slice, respectively. They work on any ordered type, and the slice does not need to be sorted.

values := []int{42, 17, 89, 3, 56}
fmt.Println(slices.Min(values)) // 3
fmt.Println(slices.Max(values)) // 89

MinFunc and MaxFunc let you compare elements with a custom function, which is essential for struct slices or when you want a non-standard ordering.

type task struct {
	name     string
	priority int
}
tasks := []task{
	{"write docs", 2},
	{"fix bug", 5},
	{"refactor", 1},
}
highest := slices.MaxFunc(tasks, func(a, b task) int {
	return a.priority - b.priority
})
fmt.Println(highest.name) // fix bug

Min and Max panic on empty slices:

Calling Min or Max on an empty slice causes a runtime panic. There is no sentinel value returned. Always check len(s) > 0 before using these functions, or use a pattern that guarantees a non-empty slice.

Iterators and Collection Utilities

The slices package includes several functions that bridge slices with Go's iterator patterns (introduced in Go 1.22 and expanded in 1.23). While they don't replace the core modification functions, they integrate slices cleanly with for range loops over sequences.

All returns an iterator over index-value pairs in the usual order. Backward traverses the slice in reverse. Values returns an iterator over just the values, without indices.

names := []string{"Alice", "Bob", "Charlie"}
for i, name := range slices.All(names) {
	fmt.Println(i, name)
}
// Output:
// 0 Alice
// 1 Bob
// 2 Charlie
for _, name := range slices.Backward(names) {
	fmt.Println(name)
}
// Output:
// Charlie
// Bob
// Alice

Collect turns an iterator back into a new slice. This is useful when you have a sequence from some other source (like a map's values or a generator) and need a concrete slice.

AppendSeq appends all values from an iterator to an existing slice and returns the extended slice.

seq := slices.Values([]int{4, 5, 6})
result := slices.AppendSeq([]int{1, 2, 3}, seq)
fmt.Println(result) // [1 2 3 4 5 6]

Chunk yields consecutive sub-slices of up to a specified size. The last chunk may be smaller. All returned sub-slices are clipped to length, with no unused capacity.

data := []int{1, 2, 3, 4, 5, 6, 7}
for chunk := range slices.Chunk(data, 3) {
	fmt.Println(chunk)
}
// [1 2 3]
// [4 5 6]
// [7]

These iterator functions make the slices package a bridge between traditional slice-based code and the more functional, pipeline-oriented style that iterators enable. They aren't mandatory to learn immediately, but they appear more often as Go's standard library evolves.

Iterator composition:

You can chain Values, AppendSeq, Collect, and other iterator-based functions from different packages (like maps.Values) to build data transformation pipelines without intermediate slices. This can reduce allocations in performance-sensitive paths.

Summary

The slices package does not introduce new concepts — it standardizes operations that every Go developer was already writing by hand. Its value is in consistency, correctness around edge cases like nil slices and capacity zeroing, and the ability to work with generic types without reflection.

When deciding whether to use a function from this package or write your own loop, consider what you gain: Delete correctly zeroes old elements, Clone avoids the shared-backing-array surprise, and Compact handles memory in a way that a naive loop easily misses. These details are small until they cause a production bug.

For further exploration, the sort package provides more nuanced sorting control, and the maps package offers analogous utility functions for maps. If you haven't yet, working through the chapter on generics will clarify how these Func variants accept custom behavior without interface boxing.