Stdlib Generic Packages — Middle Level¶
Table of Contents¶
- The
*Funcfamily — why it exists SortFunc,SortStableFunc,IsSortedFuncBinarySearchFuncContainsFuncandIndexFuncCompactFuncCollectand friends — building from iterators- Integrating
slices.SortStableFunc(s, cmp.Compare) - Multi-key sorting with
cmp.Or - Summary
The *Func family — why it exists¶
The plain APIs in slices rely on == (constraint comparable) or < (constraint cmp.Ordered). That covers basic types but leaves anything more interesting out:
- A
Personstruct with a non-comparable[]Tagfield - A slice of slices where you compare by length
- Strings compared case-insensitively
- A struct sorted by a derived key (
Age()method)
For these cases, every slices function has a *Func twin that takes a callback. The plain version is a convenience wrapper around the *Func version.
// roughly
func Contains[S ~[]E, E comparable](s S, v E) bool {
return ContainsFunc(s, func(e E) bool { return e == v })
}
In modern Go you should always check whether the *Func variant exists when the plain one does not fit your needs.
SortFunc, SortStableFunc, IsSortedFunc¶
slices.SortFunc(s, cmp) // unstable in-place sort
slices.SortStableFunc(s, cmp) // stable in-place sort
slices.IsSortedFunc(s, cmp) // bool
The comparator is a func(a, b E) int that returns: - < 0 when a < b - 0 when a == b - > 0 when a > b
Use cmp.Compare as the comparator whenever the keys are themselves cmp.Ordered:
Stable vs unstable¶
slices.Sort and slices.SortFunc use pdqsort — pattern-defeating quicksort — which is unstable. If two elements compare equal, their relative order is undefined.
slices.SortStableFunc guarantees that equal elements keep their input order. Use it when:
- The slice is already partially sorted by another key
- You implement multi-pass sorting ("sort by Age, then by Name" without
cmp.Or) - The user expects deterministic ordering for equal keys
Stable sort is slightly slower than unstable. There is no slices.SortStable (without Func) because the only stable use case requires a callback.
IsSortedFunc for invariants¶
A cheap precondition check before BinarySearch.
BinarySearchFunc¶
idx, found := slices.BinarySearchFunc(s, target, func(elem, t E) int {
return cmp.Compare(elem.Key, t.Key)
})
Two important rules:
- The slice must already be sorted by the same comparator. The function does not check.
- The callback compares an element against the target, not two elements.
When found is false, idx is the insertion point. This makes BinarySearchFunc useful for lower-bound lookups:
idx, _ := slices.BinarySearchFunc(s, target, cmp.Compare)
// idx is now the first position where elem >= target
Combining with Insert¶
O(log n) lookup + O(n) shift = ordered set without a map.
ContainsFunc and IndexFunc¶
These work on any slice — no comparable required. Use them when:
- You want substring/prefix check on
[]string - You filter by a boolean property of a struct
- The element type is a slice or map (not comparable)
hasAdmin := slices.ContainsFunc(users, func(u User) bool {
return u.Role == "admin"
})
i := slices.IndexFunc(events, func(e Event) bool {
return e.Time.After(deadline)
})
There is also slices.IndexFunc returning -1 for not-found, mirroring slices.Index.
CompactFunc¶
slices.Compact(s) // adjacent dedup with ==
slices.CompactFunc(s, equal) // adjacent dedup with callback
CompactFunc lets you deduplicate by a custom equality:
slices.CompactFunc(words, func(a, b string) bool {
return strings.EqualFold(a, b) // case-insensitive
})
Both Compact and CompactFunc only remove adjacent duplicates. Always sort first if you want full deduplication:
Dedup by key¶
slices.SortFunc(users, func(a, b User) int { return cmp.Compare(a.ID, b.ID) })
users = slices.CompactFunc(users, func(a, b User) bool { return a.ID == b.ID })
Collect and friends — building from iterators¶
Go 1.23 introduced range-over-func and the iter.Seq[T] / iter.Seq2[K, V] types. Several slices and maps functions changed accordingly.
slices.Collect¶
seq := iter.Seq[int](func(yield func(int) bool) {
for i := 0; i < 5; i++ { if !yield(i) { return } }
})
nums := slices.Collect(seq) // [0 1 2 3 4]
slices.Collect consumes an iter.Seq[T] and returns a []T. It is the canonical bridge between iterator-style code and slice-style code.
slices.Sorted and slices.SortedFunc¶
sorted := slices.Sorted(maps.Keys(m)) // sorted []K
sorted := slices.SortedFunc(maps.Keys(m), cmp.Compare)
These take an iterator and return a new sorted slice, leaving the source untouched. They internalize the Collect + Sort pair.
maps.Keys and maps.Values returning iterators¶
In Go 1.23+:
for k := range maps.Keys(m) { ... } // direct range
for k, v := range m { ... } // also still works
keys := slices.Collect(maps.Keys(m)) // explicit slice
The iterator API integrates cleanly with slices.Collect. The plain "give me a slice" idiom is still one line.
Integrating slices.SortStableFunc(s, cmp.Compare)¶
The most common pattern in modern Go:
Wait — SortStableFunc takes func(a, b T) int, and cmp.Compare has that exact signature. So the compiler accepts cmp.Compare as the function value:
In practice you usually pass a closure that calls cmp.Compare on a derived key:
Note: slices.SortStable (without Func) does not exist — you would only need stable sorting when there is a key to sort by, hence the callback is mandatory.
Multi-key sorting with cmp.Or¶
cmp.Or (Go 1.22+) returns the first non-zero argument. Combined with cmp.Compare it produces beautiful multi-key sorts:
slices.SortStableFunc(people, func(a, b Person) int {
return cmp.Or(
cmp.Compare(a.Department, b.Department),
cmp.Compare(a.LastName, b.LastName),
cmp.Compare(a.FirstName, b.FirstName),
cmp.Compare(a.Age, b.Age),
)
})
How it reads in English: "Sort by Department; if equal, by LastName; if equal, by FirstName; if equal, by Age". Each cmp.Compare returns -1/0/+1 and cmp.Or keeps walking until it finds a non-zero result.
Descending sort¶
To reverse the order on one key, negate the comparator:
slices.SortFunc(rows, func(a, b Row) int {
return cmp.Or(
cmp.Compare(a.Group, b.Group),
-cmp.Compare(a.Score, b.Score), // descending
)
})
A negative integer is also a valid "less than" outcome, and cmp.Or stops on any non-zero value.
Pattern in real code¶
// Sort tasks by priority (high to low), then by due date (early first),
// then by ID (stable)
slices.SortStableFunc(tasks, func(a, b Task) int {
return cmp.Or(
cmp.Compare(b.Priority, a.Priority), // a/b swap = descending
cmp.Compare(a.Due.Unix(), b.Due.Unix()),
cmp.Compare(a.ID, b.ID),
)
})
This idiom replaces ten lines of hand-rolled Less methods.
Worked example: paginated table sort¶
A common UI feature: a table with multi-column sort. The user clicks a header, the table sorts; clicks another column, sort changes; holds Shift, secondary sort is added. Server-side, you receive a list of "sort keys" and must sort the data accordingly.
type SortKey struct {
Field string
Desc bool
}
func sortRows(rows []Row, keys []SortKey) {
slices.SortStableFunc(rows, func(a, b Row) int {
for _, k := range keys {
c := compareField(a, b, k.Field)
if k.Desc { c = -c }
if c != 0 { return c }
}
return 0
})
}
func compareField(a, b Row, field string) int {
switch field {
case "name": return cmp.Compare(a.Name, b.Name)
case "age": return cmp.Compare(a.Age, b.Age)
case "score": return cmp.Compare(a.Score, b.Score)
}
return 0
}
The pattern combines: - SortStableFunc for stability across multiple sort cycles - cmp.Compare for typed field comparison - A switch dispatching on the field name - Inversion (-c) for descending order
This is the bread-and-butter middle-level use of the package.
Working with derived keys¶
A common need: sort by a value derived from each element, not by an element field directly. slices.SortFunc plus a small KeyFunc helper handles this:
func SortByKey[T any, K cmp.Ordered](s []T, key func(T) K) {
slices.SortFunc(s, func(a, b T) int {
return cmp.Compare(key(a), key(b))
})
}
SortByKey(users, func(u User) string { return strings.ToLower(u.Email) })
The wrapper saves the comparator boilerplate. For one-off sorts the inline closure is fine; for repeated patterns the wrapper pays for itself.
Summary¶
The middle-level skill set is knowing the *Func variants and chaining them with cmp.Compare and cmp.Or:
SortFunc/SortStableFuncfor any comparatorIsSortedFuncas a precondition guardBinarySearchFuncforO(log n)lookups in pre-sorted custom dataContainsFunc/IndexFuncfor predicate-driven searchCompactFuncfor dedup with custom equalityslices.Collect/slices.Sortedas bridges from iterator to slicecmp.Compare+cmp.Orfor multi-key sorts in one declarative expression
Once these patterns are second nature, the day-to-day Go codebase shrinks dramatically: sort.Slice calls disappear, custom Less methods disappear, hand-rolled dedup loops disappear.
Move on to senior.md for the algorithmic guarantees, aliasing rules, and when stdlib stops being the right answer.