- Book: The Complete Guide to Go Programming
- Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go
- My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools
- Me: xgabriel.com | GitHub
Open internal/util/slice.go in any Go service that was bootstrapped before 1.21. You'll find ContainsString, IndexOfInt, a hand-rolled removeAt, two versions of uniq, and a mergeSlices helper that someone copy-pasted from Stack Overflow in 2019.
Most of that file can go. Go 1.21 shipped the slices package in August 2023, and 1.22 filled in the last gap with Concat. Eleven of those functions cover the work those helpers were doing. The migration is mechanical: import "slices", delete the helper, replace the call.
There's one catch worth knowing before you swap. Delete and Compact quietly changed behavior between 1.21 and 1.22 in a way that affects pointer-heavy slices.
A note on versions
Everything below is in slices from Go 1.21 onward, except Concat, which arrived in 1.22. If you're on 1.20 or earlier, none of this works without golang.org/x/exp/slices, and you should be planning the upgrade anyway.
1. slices.Contains — the helper everyone wrote
The pre-1.21 version, written once per type:
func ContainsString(s []string, target string) bool {
for _, v := range s {
if v == target {
return true
}
}
return false
}
if ContainsString(allowed, user.Role) { ... }
After:
import "slices"
if slices.Contains(allowed, user.Role) { ... }
Contains works on any slice of comparable. No more per-type variants. If you need a predicate instead of equality, reach for slices.ContainsFunc.
2. slices.Index: same trick, position included
When you need the position, not just the boolean:
i := slices.Index(events, "user.deleted")
if i >= 0 {
events = append(events[:i], events[i+1:]...)
}
Returns -1 when nothing matches — same shape as strings.Index.
The before-version was usually a for loop with a manual counter. Less interesting, equally redundant.
3. slices.Sort: drop the sort.Slice closure
sort.Slice and sort.SliceStable still work, but they take a closure that has to capture the slice and compare by index. The new form is direct.
nums := []int{3, 1, 4, 1, 5, 9, 2, 6}
slices.Sort(nums)
// [1 1 2 3 4 5 6 9]
No closure to capture the slice, no func(i, j int) bool { return nums[i] < nums[j] } boilerplate. slices.Sort works on any cmp.Ordered type: integers, floats, strings, named types whose underlying type is one of those.
4. slices.SortFunc — when you need a comparator
For struct slices and anything where natural ordering doesn't apply.
type User struct {
ID int
Name string
Age int
}
users := []User{...}
slices.SortFunc(users, func(a, b User) int {
return cmp.Compare(a.Age, b.Age)
})
Two things are different from sort.Slice. The comparator takes the values, not indices, so it reads more directly. And it returns an int (negative, zero, positive) instead of a bool, which means three-way comparison is one line instead of a chained if/else. Pair it with cmp.Compare from the cmp package (also new in 1.21) and most sort comparators become a one-liner.
For tiebreakers, cmp.Or chains comparisons:
slices.SortFunc(users, func(a, b User) int {
return cmp.Or(
cmp.Compare(a.Age, b.Age),
cmp.Compare(a.Name, b.Name),
)
})
5. slices.BinarySearch: O(log n) when your slice is already sorted
If your slice is sorted and you're calling Contains in a loop, you're doing O(n) work where O(log n) would do. BinarySearch returns the position and a boolean for "found":
ids := []int{102, 318, 421, 559, 770, 803}
i, found := slices.BinarySearch(ids, 559)
// i=3, found=true
The position is useful even when found is false. It's where you'd insert the value to keep the slice sorted. So the pattern "insert into a sorted slice, no duplicates" is:
i, found := slices.BinarySearch(ids, newID)
if !found {
ids = slices.Insert(ids, i, newID)
}
That's two stdlib calls replacing a hand-rolled function with the off-by-one bug you've shipped twice.
6. slices.Equal — element-wise comparison without reflect
Two slices, same length, every element equal in order. Comparable element types.
slices.Equal([]int{1, 2, 3}, []int{1, 2, 3}) // true
slices.Equal([]string{"a"}, []string{"b"}) // false
The pre-slices version was either a hand-rolled loop or a reflect.DeepEqual call. reflect.DeepEqual works, but it's reflection — slow, allocating, no compile-time type guarantee. slices.Equal is generic, type-checked, and short enough to read end-to-end. It's a nine-line function.
For non-comparable elements (slices of slices, structs containing maps), use slices.EqualFunc and pass your own equality predicate.
7. slices.Compact: dedupe consecutive runs (read the doc carefully)
Compact removes consecutive duplicate elements. It does NOT do a full set-style dedupe. That's a different problem.
xs := []int{1, 1, 2, 3, 3, 3, 4}
xs = slices.Compact(xs)
// [1 2 3 4]
If you want the full dedupe, sort first, then compact:
slices.Sort(xs)
xs = slices.Compact(xs)
The naming throws people the first time. "Compact" sounds like full dedupe. It is not.
Compact also got the same 1.22 tail-zeroing fix as Delete (more on that next), so dropping references on shrink works correctly on 1.22+ without you doing anything.
8. slices.Delete — and the 1.22 gotcha worth knowing
This is the one that changed behavior between 1.21 and 1.22, and the change matters if your slices hold pointers, maps, channels, slices, or strings.
The API:
xs := []int{10, 20, 30, 40, 50}
xs = slices.Delete(xs, 1, 3) // remove indices 1..2
// [10 40 50]
In Go 1.21, Delete shifted the surviving elements left and resliced. Whatever sat in the unused tail of the backing array stayed there. If those tail elements held pointers, the GC couldn't collect what they pointed at. The slice no longer referenced them, but the backing array still did.
In Go 1.22, the behavior changed: Delete now zeroes the tail elements before returning. Same for DeleteFunc, Compact, CompactFunc, and Replace. The fix exists because the old behavior caused real memory leaks in long-lived services holding slices of *Image, *Connection, or anything that points at memory the GC needs to free.
What this means practically:
- On 1.22+, you don't have to think about it.
- On 1.21, you were responsible for nilling out the tail yourself if your elements were reference types. Most codebases didn't.
- For value-only slices (
[]int,[]float64, structs with no pointers), the change is a no-op. The zeroing is cheap and there's nothing to leak.
If you're upgrading from 1.20-and-earlier and your codebase has hand-rolled delete helpers that DO zero the tail, you can drop them. If your helpers DON'T zero the tail and you're on Go 1.22+, switching to slices.Delete is a quiet bug fix.
9. slices.Insert: the typed splice
The inverse of Delete. Insert one or more values at index i, shift the rest right.
xs := []int{10, 20, 40, 50}
xs = slices.Insert(xs, 2, 30)
// [10 20 30 40 50]
ys := []string{"a", "d"}
ys = slices.Insert(ys, 1, "b", "c")
// [a b c d]
Variadic, so you can splice in a single value or a small batch. For inserting a whole slice, use Concat (next).
A 1.22 detail: Insert now panics consistently when i is out of range, even when there are no values to insert. In 1.21 it would silently succeed in the no-values case. This is a small thing but it means your bounds checks behave the same regardless of input.
10. slices.Concat: the late arrival from 1.22
The only function in this list that didn't ship in 1.21. Concatenate any number of slices into a new one:
a := []int{1, 2}
b := []int{3, 4}
c := []int{5, 6}
all := slices.Concat(a, b, c)
// [1 2 3 4 5 6]
The pre-Concat version was either:
all := append(a, b...)
all = append(all, c...)
…which has the classic slice-aliasing trap: if a had spare capacity, the first append mutates it. slices.Concat allocates a fresh slice with the right capacity and copies into it. No aliasing, no surprises. For two slices it's not much shorter. For five or six it cleans up the call site significantly.
11. slices.Min and slices.Max: one entry, two functions
Counted as one because they ship together and you reach for them in the same situations.
prices := []float64{12.5, 9.99, 14.0, 7.50}
cheapest := slices.Min(prices) // 7.50
priciest := slices.Max(prices) // 14.0
Important: both panic on empty slices. There's no (value, ok) form. If your input might be empty, check len first or use slices.MinFunc/MaxFunc with a custom comparator. Those panic on empty too. The panic on empty input is the contract; the only choice is whether you want a custom comparator on top.
The built-in min/max (also new in 1.21) are different. They work on a fixed list of arguments, not a slice. min(a, b, c) versus slices.Min([]int{a, b, c}). Use the built-ins for argument lists, the slice form for collections.
What to delete from your codebase tomorrow
In rough order of how often it pays off:
- Per-type
Contains,Indexhelpers. One line ofslices.Containsper call site. -
sort.Slicecalls with simple comparators.slices.Sortfor ordered,slices.SortFuncwithcmp.Compareotherwise. -
reflect.DeepEqualon slices of comparable elements.slices.Equalis faster and type-safe. - Hand-rolled "remove element at index" code.
slices.Delete, with the bonus tail-zeroing on 1.22+. - Linear "is this in this sorted slice" loops.
slices.BinarySearch.
Run gopls after the migration; it flags most of these old patterns now.
The slices package isn't really about shorter code. It's that slice-handling stops being your problem. The Go team maintains the test suite for these functions, and the 1.22 zeroing change landed in your Delete calls without you doing anything.
If this was useful
If the slice patterns above were the kind of thing you wished someone had explained in your first year of Go, The Complete Guide to Go Programming covers the language at the same level. What the spec actually says, what the runtime actually does, and which of your habits from other languages don't translate. The hexagonal-architecture book picks up where that one leaves off, for when the codebase is large enough that "where does this slice helper live" stops being a small question.

Top comments (0)