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.
slices PackageGo 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:
slices.Contains([]string{"a","b"}, 1) doesn't compile. The same call against an interface{}-based helper would compile and explode at runtime.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.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 is the helper most teams use first. The package gives you three variants that cover almost every case.
| Function | When 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.
Func VariantsWhen you don't need sorted-access speed, plain linear lookups are simpler. Four functions cover the common cases.
| Function | What it returns | Use 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 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.
| Function | What 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.
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.
Func Variantsslices.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.
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.
| Function | What 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.
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."
10 quizzes