AlgoMaster Logo

The slices Package

Medium Priority22 min readUpdated June 6, 2026

The slices package (added to the standard library in Go 1.21) gives you a generic toolbox for the operations every Go program ends up writing by hand: sorting, searching, deleting, comparing, deduplicating. Before generics, each of those helpers had to be rewritten per element type or hidden behind interface{} casts. Now there's one type-safe, well-tuned implementation that works for any slice. This lesson walks the API in the order you'll use it on a real codebase, with an e-commerce running example.

Why a Generic slices Package

Go 1.18 added generics. Go 1.21 cashed in on them by shipping slices and maps in the standard library, so you no longer need to write sortByPrice, containsProductID, removeFromCart, and so on for every element type in your project.

Three things change once you adopt slices:

  1. Type safety. slices.Contains([]string{"a","b"}, 1) doesn't compile. The same call against an interface{}-based helper would compile and explode at runtime.
  2. Speed. slices.Sort uses pdqsort (pattern-defeating quicksort), which is faster than the older sort.Sort for almost every realistic input because it skips full sorts on already-sorted or nearly-sorted data.
  3. One name per operation. Every Go codebase used to have its own removeAt, dedupe, reverse. Now everyone reaches for slices.Delete, slices.Compact, slices.Reverse.

Both calls are fully type-safe. Sort takes a []E where E is any ordered type, and Contains takes a []E plus an E. Pass the wrong element type and the compiler stops you.

The diagram groups the API the way you'll use it in practice: sort/search, find, and mutate. Every helper accepts a slice as input and either returns a value, a new slice, or both. None of the helpers spawn goroutines or do I/O. They're pure data operations.

Sorting and Searching

Sorting is the helper most teams use first. The package gives you three variants that cover almost every case.

FunctionWhen to use
slices.Sort(s)Slice of an ordered type (int, float64, string, etc.)
slices.SortFunc(s, cmp)Custom comparison (e.g., sort products by price)
slices.SortStableFunc(s, cmp)Custom comparison, preserve relative order of equal elements

The comparison function returns a negative number if a < b, zero if a == b, and a positive number if a > b. Go 1.21 added the cmp package which gives you cmp.Compare and cmp.Or for chaining comparisons, so you rarely have to write that arithmetic by hand.

The two products at $49.99. Their order relative to each other isn't guaranteed by SortFunc. If you need a stable sort (input order preserved among equal-priced items), use SortStableFunc. The trade-off is a small constant-factor slowdown.

For multi-key sorting, chain comparisons with cmp.Or, which returns the first non-zero result.

Now ties on price break alphabetically by name. That's the idiomatic pattern in Go 1.21+: one cmp.Or chain expresses "sort by A, then by B, then by C" without nested if-statements.

Once a slice is sorted, you can binary-search it. slices.BinarySearch returns an index and a bool, the index where the value is (or would be inserted if missing), and whether an exact match was found.

The second call tells you "B005" isn't in the slice but would sort at index 3, which is exactly where you'd insert it to keep things sorted. slices.BinarySearchFunc does the same with a custom comparator, useful when the slice holds structs.

Sort is O(n log n) and BinarySearch is O(log n). Linear scans (Contains, Index) are O(n). If you query the same slice many times, sorting once and binary-searching is the better trade.

Searching: Contains, Index, and Their Func Variants

When you don't need sorted-access speed, plain linear lookups are simpler. Four functions cover the common cases.

FunctionWhat it returnsUse when
Contains(s, v)bool"Is v in this slice?"
ContainsFunc(s, pred)bool"Does anything in this slice match this predicate?"
Index(s, v)int (-1 if missing)"Where is v in this slice?"
IndexFunc(s, pred)int (-1 if missing)"Where's the first element matching the predicate?"

The Func variants take a predicate (a func(E) bool) instead of a target value. Use them when "equality" isn't a simple ==, for example, finding the first out-of-stock product.

ContainsFunc short-circuits: it stops at the first match. IndexFunc does the same but reports where. For "find them all" use cases, range over the slice yourself or filter with a comprehension-style loop.

Contains and Index work only when the element type is comparable with ==. That includes the basic types, strings, pointers, channels, arrays of comparable types, and structs whose fields are all comparable. Slices, maps, and functions aren't comparable, so a slice of those needs the Func variants.

A more realistic search example combines Index and IndexFunc to inspect a cart. To want to know whether a specific SKU sits in the cart, and separately whether anything in the cart is currently discounted.

Index handles the simple "find this exact string" case. IndexFunc carries the load when the matching rule is anything more interesting than equality. Both return -1 if nothing matches, so you always check >= 0 before indexing into the slice.

Mutation: Insert, Delete, Replace, Compact, Reverse

Mutation helpers are where slices saves the most code. Every Go developer has written some version of s = append(s[:i], s[i+1:]...). It works, it's idiomatic, but it's also easy to get wrong (off-by-one on the slice bounds, forgetting to reassign, accidentally aliasing the backing array).

The package replaces those one-liners with named operations that mean what they say.

FunctionWhat it does
Insert(s, i, v...)Insert one or more values starting at index i
Delete(s, i, j)Remove elements from index i to j (exclusive)
DeleteFunc(s, pred)Remove every element where the predicate is true
Replace(s, i, j, v...)Replace s[i:j] with v...
Compact(s)Collapse consecutive equal elements into one
CompactFunc(s, eq)Same, with a custom equality function
Reverse(s)Reverse in place

Three things are worth pinning down.

First, Insert, Delete, and Replace all return a new slice header. They may or may not allocate a new backing array depending on whether the existing capacity is enough. Always reassign: cart = slices.Delete(cart, 1, 2). Forgetting the assignment is the most common bug with these helpers.

Second, Reverse is the exception. It mutates in place and returns nothing. You don't assign it. That's because reversing never needs to grow the slice.

Third, Delete and DeleteFunc zero out the freed tail slots when the element type contains pointers. This matters because a tail slot still inside cap(s) could otherwise keep a *Product alive past its useful life and confuse the garbage collector. Since Go 1.22, this zeroing is the documented behavior, you don't have to do it yourself.

DeleteFunc shifts surviving elements left and returns the shortened slice. The backing array's capacity doesn't shrink, only the length does. If you need to free the underlying memory, slices.Clip(s) returns a slice whose capacity equals its length.

CompactFunc does the same job as Compact but with a custom equality predicate, which matters when you want to dedupe by a derived key rather than full struct equality. Here's an example that collapses consecutive products that share the same category, keeping only the first per run.

CompactFunc keeps the first product in each consecutive run that shares a category. This pattern is handy for collapsing sorted event streams: sort by timestamp, then compact by source to keep only the first event per source per run.

Compact deserves its own example because the name confuses people. It removes _consecutive_ duplicates, not all duplicates. To dedupe an unsorted slice, sort first, then compact.

The first call collapses neighbors. The second sorts first so equal values become neighbors, then collapses them. Sort-then-compact is the idiomatic Go way to dedupe.

Compact is in-place and O(n). Sort-then-Compact is O(n log n) total. If order doesn't matter and the input is large, that combo is faster than building a map[T]struct{} to dedupe, because it avoids hash overhead and allocates almost nothing.

Copying, Equality, and Concatenation

When you pass slices around, every Go developer eventually hits the aliasing problem: two slice variables share a backing array, one mutates, the other sees the change. slices.Clone cuts that bond.

Clone allocates a fresh backing array of the same length and copies every element. It's a shallow copy: if your slice holds pointers or other slices, those inner references are shared. For deeply nested structures, you write the deep clone yourself.

Clone matters most when you return a slice from a function that holds the canonical copy internally. Without cloning, your caller can mutate your internal state by accident.

The cart's internal state is untouched because the caller mutates the clone, not the original. Without slices.Clone, the assignment would corrupt the cart.

Equality has its own pair of helpers.

Equal does element-wise ==, returning true only if lengths match and every pair is equal. EqualFunc swaps in your own predicate. Both are O(n) and short-circuit on the first mismatch.

Concat joins multiple slices into one new slice. It pre-counts the total length and allocates once, which is faster than chained append calls when you have three or more inputs.

Concat returns a fresh slice. The inputs are untouched, so this is a safe operation even when the source slices are shared.

One more practical pattern: combining Clone with Sort to sort without mutating the input. This is the standard way to compute a "sorted view" of a slice while leaving the original alone for other callers.

This pattern shows up everywhere: in HTTP handlers that need to return a sorted JSON array without mutating shared state, in test helpers that want to compare slices regardless of input order, and in any code path where the same slice is read by multiple goroutines.

Aggregates: Max, Min, and Their Func Variants

slices.Max and slices.Min return the largest and smallest element in a non-empty slice. They panic on an empty input, which is the same convention math.Max follows on NaN. If your slice might be empty, check length first.

MaxFunc and MinFunc take a comparator and return the element itself, not its index. If you need the index, use IndexFunc after finding the element, or write a single pass that tracks both.

The Func variants are useful when you can't use < on the element type, like comparing structs by a derived field or comparing strings by length.

If you want both the priciest product and its index, write the loop yourself, because MaxFunc discards positional information.

A hand-rolled loop costs five extra lines and avoids two passes. For small slices either approach is fine; for slices in the millions, the single-pass version saves a measurable amount of work.

Integration with Iterators

Go 1.23 added range-over-function and the iter package. The slices package gained matching helpers that bridge slices and iterators in both directions.

FunctionWhat it does
slices.All(s)Yields (index, value) pairs from s
slices.Values(s)Yields values only
slices.Backward(s)Yields (index, value) from right to left
slices.Collect(seq)Drains an iterator into a []E
slices.Sorted(seq)Drains an iterator, returns a sorted slice
slices.SortedFunc(seq, cmp)Same with a custom comparator
slices.AppendSeq(s, seq)Appends iterator output to an existing slice

These let you use slices anywhere an iterator is expected, and vice versa, without writing a loop to bridge the two worlds.

Backward is a tidy alternative to the classic for i := len(s) - 1; i >= 0; i-- loop. It works with any function that takes a iter.Seq2[int, E].

The other direction is just as useful. If a function returns an iterator, you can collect its output into a slice with one call.

Collect materializes the iterator. Sorted collects and then sorts in a single call. If you have an unsorted iterator and you need a sorted slice, Sorted saves the intermediate variable.

The reverse direction matters too. slices.Values lets you pass a slice anywhere an iterator is expected. Consider a function that aggregates prices from any iter.Seq[float64]: you can feed it a slice directly without writing a wrapper.

slices.Values adapts the slice into an iterator without copying. The bridge is cheap, no allocations, no intermediate slice. The reader of total doesn't need to know whether the price source is a slice, a database cursor, or a generator function.

Collect allocates a fresh slice and grows it as it consumes. If you know the upper bound on the iterator's length, pre-allocating with make([]E, 0, n) and using slices.AppendSeq can shave allocations in hot paths.

Performance Notes

A few performance facts when you use slices in hot paths.

Sort uses pdqsort, a hybrid of quicksort, insertion sort, and heapsort. On random input it behaves like quicksort. On already-sorted, reverse-sorted, or partially-sorted input it detects the pattern and shortcuts. The worst case is O(n log n), without the O(n^2) blowup that classical quicksort has on bad pivots.

Compact, Delete, DeleteFunc, and Reverse work in place. They don't allocate. Compact returns a shortened slice; the original capacity stays the same. If you want the memory back, follow with slices.Clip.

Clone allocates exactly once: a new backing array of the same length. The slice header is fresh too. This makes Clone appropriate when you want a defensive copy of a returned slice.

Concat pre-sums the lengths of the inputs and allocates exactly once. That's faster than result = append(result, a...); result = append(result, b...); result = append(result, c...), which can trigger multiple reslices.

BinarySearch is O(log n) and branchless on most platforms, since recent versions of Go compile it without a hot conditional jump. That makes it noticeably faster than the equivalent hand-rolled loop on large sorted slices.

Most other helpers (Contains, Index, Equal, Max, Min) are straightforward O(n) linear scans with short-circuit on the first hit or mismatch. The constant factors are tight because the package was written with benchmarks in mind, you'd be unlikely to beat them with a hand-written version unless you specialize for a fixed type.

that contrasts the allocation behavior of Concat against chained append calls.

Concat sizes the result to exactly len(a) + len(b) + len(c). The chained append version grows the backing array geometrically and lands on the next power-of-two-ish capacity, which is fine for one-off code but wasteful in tight loops.

The other helper about is slices.Grow. It pre-extends a slice's capacity so subsequent append calls don't reallocate. If you know you're about to add n items in a loop, s = slices.Grow(s, n) gets the allocation done up front.

Grow doesn't change the length, only the capacity. It's the standard library answer to "I know exactly how many items I'm about to append; please don't make me reallocate three times."

Quiz

slices Package (Modern) Quiz

10 quizzes