Golang Sorting and Custom Sorting by Functions
Golang 12 min read
The slices package replaced most of sort, SortFunc wants a three-way comparison where sort.Slice wanted a boolean, and neither is stable unless you ask.
Go 1.21 added the slices package, and it changed the idiomatic answer to sorting. sort.Slice still
works and always will; slices.SortFunc is generic, faster in most cases, and takes a different
kind of comparison function, a three-way integer rather than a boolean, which is the detail that
turns a mechanical migration into a broken sort.
Written against Go 1.22.
The simple cases
package main
import (
"fmt"
"slices"
)
func main() {
nums := []int{5, 2, 9, 1}
words := []string{"pear", "apple", "fig"}
slices.Sort(nums) // [1 2 5 9]
slices.Sort(words) // [apple fig pear]
fmt.Println(slices.IsSorted(nums)) // true
fmt.Println(slices.Reverse(nums)) // reverses in place, no return value
fmt.Println(slices.Max(nums), slices.Min(nums))
}
slices.Sort works on any slice of an ordered type, every integer and float kind, and string.
There is no comparison function because cmp.Ordered already defines one.
It sorts in place and returns nothing. sorted := slices.Sort(x) is a compile error, which is a
kindness: the older sort.Ints had the same behaviour and no such guard.
slices.Reverse also mutates in place. For descending order, sorting and then reversing is two passes;
one pass with a comparison is better:
slices.SortFunc(nums, func(a, b int) int { return cmp.Compare(b, a) })
SortFunc: three-way, not boolean
This is the migration trap:
// sort.Slice — a "less" predicate
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})
// slices.SortFunc — a three-way comparison
slices.SortFunc(people, func(a, b Person) int {
return cmp.Compare(a.Age, b.Age)
})
sort.Slice takes indices and returns a bool. slices.SortFunc takes values and returns
a negative number, zero, or a positive number.
Returning a bool from SortFunc does not compile, so that half is caught. What is not caught is
returning a.Age - b.Age: it compiles. It works for small values, and it inverts on overflow when the
difference exceeds the integer range. cmp.Compare is the correct form and reads better anyway.
Zero has to mean “equal”. A comparison that never returns zero: if a.Age <= b.Age { return -1 } —
violates the contract, and the sort’s behaviour on such input is undefined rather than merely
suboptimal.
Multiple keys
slices.SortFunc(people, func(a, b Person) int {
if c := cmp.Compare(a.LastName, b.LastName); c != 0 {
return c
}
if c := cmp.Compare(a.FirstName, b.FirstName); c != 0 {
return c
}
return cmp.Compare(a.Age, b.Age)
})
cmp.Or shortens it, since it returns the first non-zero argument:
slices.SortFunc(people, func(a, b Person) int {
return cmp.Or(
cmp.Compare(a.LastName, b.LastName),
cmp.Compare(a.FirstName, b.FirstName),
cmp.Compare(a.Age, b.Age),
)
})
The difference is that cmp.Or evaluates every argument, so a comparison expensive enough to matter
still wants the early-return form.
Stability
Neither slices.Sort nor slices.SortFunc is stable: equal elements can be reordered, and the order
can differ between runs on the same input. slices.SortStableFunc preserves the original order of
equal elements, at some cost.
slices.SortStableFunc(people, func(a, b Person) int {
return cmp.Compare(a.Department, b.Department)
})
Stability is what makes successive sorts compose. Sorting by name and then stably by department produces departments ordered internally by name. Without it, the second sort discards the first.
The alternative is to put every key in one comparison, which is cheaper and states the intent in one place. Reach for stability when the existing order carries meaning that no key expresses, an arrival sequence, a line number in a file.
The older sort package
sort.Slice(people, func(i, j int) bool { return people[i].Age < people[j].Age })
sort.SliceStable(people, func(i, j int) bool { return people[i].Age < people[j].Age })
sort.Slice uses reflection to swap elements, which is why the generic version is faster. It remains
correct and there is no urgency to rewrite working code.
sort.Interface is the original form and still the right answer when the data is not a slice, or when
the sort order belongs to the type:
type ByAge []Person
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
sort.Sort(ByAge(people))
Three methods for what a closure does in one line, which is why it is now rare. sort.Reverse wraps
any sort.Interface and inverts it, which is the one thing the closure form cannot do generically —
though with generics available, a small helper that flips a comparison function covers the same
ground.
Searching a sorted slice
i, found := slices.BinarySearch(nums, 5)
i, found := slices.BinarySearchFunc(people, target, func(a, b Person) int {
return cmp.Compare(a.Age, b.Age)
})
The comparison must match the one used to sort, or the result is wrong with no error. i is the
insertion point when found is false, which is what makes it useful for maintaining a sorted slice:
i, _ := slices.BinarySearch(nums, value)
nums = slices.Insert(nums, i, value)
Sorting by a derived key
When the sort key is expensive to compute (parsing a date, lowercasing for a case-insensitive comparison, calling a method) computing it inside the comparison recomputes it O(n log n) times.
The fix is to compute each key once and sort pairs:
type keyed struct {
key string
value Article
}
pairs := make([]keyed, len(articles))
for i, a := range articles {
pairs[i] = keyed{strings.ToLower(a.Title), a}
}
slices.SortFunc(pairs, func(a, b keyed) int { return cmp.Compare(a.key, b.key) })
for i, p := range pairs {
articles[i] = p.value
}
That is the decorate-sort-undecorate pattern, and it is worth the extra allocation whenever the key costs more than a field access. For a plain field comparison it is pure overhead.
The case-insensitive example is worth a caution of its own: strings.ToLower is not a correct
case-folding for every language, and sorting human-readable text alphabetically is a
locale-dependent problem that neither slices nor sort attempts to solve. golang.org/x/text/collate
is the package for that, and reaching for it is a decision about whether the ordering is shown to
people or consumed by a program.
What the sort actually costs
slices.Sort uses pattern-defeating quicksort: quicksort with fallbacks that keep the worst case at
O(n log n) rather than O(n²), plus insertion sort for short runs and a check for already-sorted
input. The practical consequence is that adversarial input cannot degrade it, which the older
quicksort in sort could not promise.
SortStableFunc uses an in-place merge with O(n log² n) comparisons, measurably slower, which is
the cost of stability. Two facts follow: sorting an already-sorted slice is close to free, and
sorting a large slice repeatedly to maintain order is worse than inserting in the right place with
BinarySearch and slices.Insert.
Sorting a map
Maps have no order, and iterating one is deliberately randomised. Sorting means extracting first:
keys := make([]string, 0, len(counts))
for k := range counts {
keys = append(keys, k)
}
slices.Sort(keys)
for _, k := range keys {
fmt.Println(k, counts[k])
}
maps.Keys returns an iterator in Go 1.23, and slices.Sorted(maps.Keys(m)) collapses the whole
thing to one line.
To sort by value, build a slice of pairs and sort that. There is no way to order a map itself. The common case, ranking word counts, is a comparison on the value with the key as a tie-breaker, so that equal counts come out alphabetically rather than arbitrarily.
More Go walkthroughs in the Golang guides, including slices for what the underlying operations cost.
Frequently asked questions
slices.Sort or sort.Slice?
slices.Sort and slices.SortFunc for new code, generic, faster, and
no reflection. sort.Slice is still correct, so existing code needs no rewrite.
Why does my SortFunc comparison not compile?
It returns a bool. slices.SortFunc wants a negative
number, zero or a positive number, not a “less” predicate.
Can I return a - b from a comparison?
It compiles and it overflows. Use cmp.Compare, which is
correct for the whole range and clearer.
Is Go’s sort stable?
No. Use slices.SortStableFunc or sort.SliceStable when the order of equal
elements matters.
How do I sort in descending order?
Compare in the other direction: cmp.Compare(b, a) — rather
than sorting and reversing, which is two passes.
How do I sort by more than one field?
Compare each key in turn and return the first non-zero
result. cmp.Or expresses that in one expression, at the cost of evaluating every argument.
Why does slices.Sort return nothing?
It sorts in place. Copy the slice first with slices.Clone
if the original must be preserved.
How do I sort a map?
Extract the keys into a slice, sort that, and index the map in order. Map iteration order is randomised deliberately and cannot be sorted.
Does BinarySearchFunc need the same comparison as the sort?
Yes. A mismatch produces a wrong answer silently: binary search cannot detect that the slice is not ordered the way it assumes.
When is sort.Interface still worth implementing?
When the collection is not a slice, or when the
ordering belongs to the type and should be reused. sort.Reverse also needs it.