AlgoMaster Logo

sort Package

Medium Priority19 min readUpdated June 6, 2026

Sorting shows up in almost every program that displays a list to a user or feeds data into another algorithm. The sort package, part of Go's standard library since version 1, provides a small set of generic-feeling tools to order slices of built-in types, slices of anything via a comparator function, and arbitrary collections that implement a three-method interface. It also includes a binary search routine for finding values in sorted data. This chapter covers each of those tools, when to use which, and the trade-offs (stability, transitivity, allocation) that decide whether your sort gives you the result you expected.

Built-in Helpers for Common Slice Types

The fastest way to sort a slice of int, float64, or string is the package-level helper for that type. There's one function per type, each accepts the slice directly, and each sorts it in place in ascending order.

The helpers mutate the slice in place and return nothing. There's no "give me a sorted copy" version in the package, so if you need to preserve the original, copy it with slices.Clone (or append([]T(nil), s...)) before sorting.

Strings sort by Unicode code point order, not by any locale-aware collation. That means "Z" (0x5A) sorts before "a" (0x61), and characters outside ASCII (accented letters, ideographs) sort by their numeric code point. For a customer-facing list that needs proper alphabetical order for non-English names, use golang.org/x/text/collate instead.

HelperSignatureSorts
sort.Intsfunc(a []int)[]int ascending
sort.Float64sfunc(a []float64)[]float64 ascending, with NaN treated as less than any number
sort.Stringsfunc(a []string)[]string by code point
sort.IntsAreSortedfunc(a []int) boolreports whether the slice is already sorted
sort.Float64sAreSortedfunc(a []float64) boolsame for floats
sort.StringsAreSortedfunc(a []string) boolsame for strings

The *AreSorted helpers are useful when you want to skip a sort that isn't needed (sorting an already-sorted slice still costs O(n) just to walk it, which the underlying algorithm short-circuits, but the check is even cheaper).

sort.Slice with a Comparator

The helpers only cover three types. For anything else, including slices of structs, slices of named types, or any custom ordering, sort.Slice takes the slice and a "less" function and sorts it in place.

The signature is sort.Slice(slice any, less func(i, j int) bool). The comparator receives two indices into the slice, not two elements, and answers a yes/no question: should the element at i come before the element at j? Go calls this comparator many times during the sort; it has to be cheap and consistent.

The function is called a less-func, and the semantics are strict less-than: return true only when slice[i] should come before slice[j], never when they're equal. Returning true for equal elements is the most common comparator bug, and it can make the sort do extra work or produce non-deterministic output across Go versions.

Three useful patterns show up over and over:

The multi-key form is worth getting comfortable with. When two products have the same price, the function falls through to the name comparison, which decides the tie. Without that fallthrough, ties would resolve in whatever order the underlying algorithm happened to land on, which is not stable in sort.Slice.

sort.SliceStable for Order-Preserving Sorts

sort.Slice is not stable: when two elements compare equal, their relative order in the output is unspecified. For most code that doesn't matter, but it matters a lot for multi-step sorts. If you sort orders by date and then re-sort the result by customer, you'd like orders for one customer to still be in date order. An unstable sort would scramble them.

Within each customer group, the orders stay in their original ID order (1, 3, 5 for Ana; 2, 4 for Bo). A non-stable sort might have produced (3, 5, 1) or (5, 1, 3) for Ana, because the comparator considers them all equal on the customer field.

The diagram shows what stability buys you. Both functions produce a list ordered by customer; the difference is what happens to elements that tie on the sort key. Stable sorts hold them in their input order, unstable sorts may rearrange them.

sort.Slicesort.SliceStable
AlgorithmQuicksort variant (pdqsort since Go 1.19)Stable variant of insertion sort + merging
Time complexityO(n log n) average and worstO(n log n) worst case
Stable?NoYes
When to useOne-shot sort, or ties don't matterMulti-key sorts done one key at a time, UI lists where input order is meaningful

If you find yourself building a complex multi-key comparator just to keep groups in their original order, stable sort by the group key alone might be simpler and faster.

sort.Interface for Custom Types

sort.Slice covers slices. For non-slice collections, or for cases where the sort logic belongs with the type itself, the package exposes a three-method interface that any collection can satisfy.

Len reports how many elements there are. Less is the same comparator semantics as sort.Slice. Swap exchanges the elements at two indices. The package's sort.Sort function takes any value that satisfies this interface and sorts it in place using the three methods.

ByPrice is a named slice type whose three methods satisfy sort.Interface. The pattern looks heavier than sort.Slice for one comparator, and for one comparator it usually is. The interface form pays off when you have several orderings for the same data and want them as named types:

Now sort.Sort(ByPrice(products)) and sort.Sort(ByName(products)) read clearly at the call site. For one-off sorts this is overkill, but for a domain type with several canonical orderings it documents intent.

The interface also unlocks sorting things that aren't slices. A custom linked-list-backed collection, an array-of-arrays, or any structure that can answer the three questions can plug into sort.Sort. The package itself reuses the interface internally: sort.IntSlice, sort.Float64Slice, and sort.StringSlice are predeclared named types that implement it, and sort.Ints is roughly sort.Sort(IntSlice(a)) internally.

The diagram shows the role sort.Interface plays. Every sort function in the package is built around it, including the helpers like sort.Ints that hide it from you. When you implement the interface yourself, you're using the same plug point the standard library uses.

For stable sorts on sort.Interface, there's a matching sort.Stable function. Same arguments, same semantics, stable guarantee.

Reverse Sorting

For reverse order, you have two options. Flip the comparator (> instead of <), or wrap an existing sort.Interface with sort.Reverse.

sort.Reverse takes a sort.Interface and returns a new one with the same Len and Swap but with Less inverted. It's a thin wrapper, no copying involved. The result is then passed to sort.Sort like any other.

For sort.Slice, you'd just write the comparator backwards, which is shorter:

Both forms produce the same result. The sort.Reverse form is most useful when you already have a named type that implements sort.Interface and you want both directions without duplicating the methods.

A subtle but useful detail: sort.Reverse does not reverse stability behavior. If you wrap a sort.Interface and pass it to sort.Stable, the result is still stable in the reversed order. Wrap-and-flip doesn't change which sort algorithm runs underneath.

sort.Search: Binary Search on Sorted Slices

Once a slice is sorted, finding a value in it should take O(log n), not O(n). The package's sort.Search does the binary search, but with an unusual signature: instead of a value and a comparator, it takes a length and a predicate.

It returns the smallest index i in [0, n) for which f(i) is true, or n if no such index exists. The predicate must be monotonic: false for some prefix of indices, then true for the rest. That's exactly the property a binary search needs.

The most common use is "find the first index whose element is greater than or equal to a target":

The "insertion point" behavior is what makes sort.Search useful even when the value isn't in the slice. If you're maintaining a sorted slice and want to insert a new value in the right place, sort.Search gives you the index to insert at:

The slice stays sorted after the insertion. Building a list this way (sorted-insert each new element) is O(n) per insertion because of the copy, but for small lists or batched inserts it's often simpler than re-sorting the whole thing.

The diagram shows the conceptual layout. sort.Search doesn't actually evaluate the predicate at every index, it does a binary search that probes O(log n) indices and converges on the boundary between the false prefix and the true suffix. The drawing shows the property the predicate must have: false, false, false, then true, true forever. As long as the predicate has that shape, binary search returns the boundary.

For convenience, the package also provides SearchInts, SearchFloat64s, and SearchStrings, which wrap the typed equivalents:

These are just wrappers around sort.Search with the predicate baked in. They're convenient when you have a plain []int, []float64, or []string and want a one-liner.

What's Wrong With This Code?

Comparator bugs are the most common source of trouble with sort.Slice. Here's one that compiles fine, runs without panic, and produces wrong output.

The author wanted "high-value orders (over $30) at the top, then everything else in ascending order". The comparator looks reasonable, but it's not transitive. Consider three orders A=$50, B=$10, C=$20:

  • Less(A, B): A.Total > 30, return true. A < B.
  • Less(B, C): B.Total > 30 is false, so check B.Total < C.Total, which is 10 < 20, true. B < C.
  • Less(A, C): A.Total > 30, return true. A < C.

So A < B and B < C and A < C, which seems consistent. But:

  • Less(B, A): B.Total > 30 is false, check B.Total < A.Total, which is 10 < 50, true. So B < A.

Now A < B AND B < A. The comparator says both are smaller than the other, which is a contradiction. The sort algorithm depends on a strict total order to make decisions, and when the comparator violates it, the result is whatever the algorithm happens to produce for that input, possibly different across Go versions, possibly an infinite loop in older Go releases.

Fix:

Now the comparator first decides which "bucket" each element is in (high or low), then sorts within the bucket. The order is total and consistent: any two elements have a single, well-defined "less" relationship.

The general rule: if your comparator's answer depends on only one of the two elements, double-check it. A correct less-func always uses both i and j symmetrically in some way.

Modern Alternative: slices.Sort and slices.SortFunc

Since Go 1.21, the slices package (part of the standard library) offers generic sort functions that are usually shorter and faster than the sort package equivalents. It's worth seeing the equivalents now so you know what newer code looks like.

Two differences worth noting. First, slices.SortFunc uses a cmp-func returning -1, 0, or 1, not a less-func. The signature is func(a, b T) int, taking elements directly instead of indices. That avoids a whole class of comparator bugs (you can't get the slice indexing wrong) and reads more naturally for many sorts. Second, slices.Sort works on any ordered type via generics, so there's no slices.SortStrings or slices.SortFloat64s.

Old (sort)New (slices)Notes
sort.Ints(s)slices.Sort(s)Works on any ordered type
sort.Slice(s, less)slices.SortFunc(s, cmp)cmp returns int, not bool
sort.SliceStable(s, less)slices.SortStableFunc(s, cmp)Same stability guarantee
sort.SearchInts(s, x)slices.BinarySearch(s, x)Returns (index, found) tuple
sort.Sort(impl)(no direct replacement)Use slices.SortFunc on the underlying slice

For new code, prefer the slices package. For maintenance work, library code that must support older Go versions, or code that sorts non-slice collections via sort.Interface, the sort package is still appropriate.

When Sorting Fails Silently

A few patterns produce wrong output without panicking, which makes them harder to debug than crashes. Knowing them helps you spot them in code review.

Comparator that isn't transitive. Already covered. If Less(a, b) and Less(b, c) are true, Less(a, c) must also be true. If the comparator violates this, the sort's output is undefined.

Comparator that uses external state. If the less-func reads a variable that another goroutine is writing, or that changes during the sort, the comparator's answer for the same pair of elements may flip mid-sort. The package documentation calls this "the Less function does not establish a strict weak ordering", and the result is the same kind of garbage as a non-transitive comparator.

Sorting a slice while iterating it. Calling sort.Slice inside a loop that's also reading the slice can produce surprising results because the indices shift under you. Sort first, then iterate.

Floats with NaN. NaN < NaN is false, and NaN > NaN is also false. A comparator that uses < directly will treat NaN as equal to everything, which is not transitive. sort.Float64s handles this by placing NaN values at the start of the result; sort.Slice with a naive comparator does not. If your data might contain NaN, filter it out or use math.IsNaN in the comparator.

The branches put NaNs at the front and sort everything else normally. It's verbose but correct, which is what you want.

Quiz

sort Package Quiz

10 quizzes