Skip to content
gohacks.dev
Basics

Sort Anything in Go With the sort and slices Packages

Sort slices of numbers, strings and structs with slices.Sort and SortFunc, sort by several keys, keep ties stable, sort map keys, and see what each API costs.

7 min read
Summarize in AI
Flat illustration of an editor window full of code.

Sorting in Go moved in 2023. The sort package, with its Interface and sort.Slice, was the only tool for a decade, and Go 1.21 added slices.Sort and slices.SortFunc, which are shorter, type safe and faster. Old code and old answers still use the old API, and it still works, so you need to read both and write the new one. This how-to covers the cases in the order you meet them: plain values, structs, multiple keys, stability and maps, with a benchmark at the end for what it costs. It follows slices and the cmp ideas in comparable types, and it is part of the Go basics path.

Tested with Go 1.25.6, macOS on Apple Silicon (arm64, Apple M1 Max)

One program, six sorts

Every sort below runs on the same small data, and the output shows each result:

main.goGo
package main

import (
	"cmp"
	"fmt"
	"maps"
	"slices"
	"sort"
	"strings"
)

type Person struct {
	Name string
	Age  int
	City string
}

func main() {
	nums := []int{5, 2, 8, 2, 9}
	slices.Sort(nums)
	fmt.Println(nums, slices.IsSorted(nums))

	words := []string{"cherry", "Banana", "apple", "Fig"}
	sort.Strings(words)
	fmt.Println(words)
	slices.SortFunc(words, func(a, b string) int {
		return cmp.Compare(strings.ToLower(a), strings.ToLower(b))
	})
	fmt.Println(words)

	people := []Person{
		{"ada", 36, "London"},
		{"grace", 45, "Boston"},
		{"linus", 28, "Boston"},
		{"alan", 36, "London"},
	}

	slices.SortFunc(people, func(a, b Person) int {
		return cmp.Compare(a.Age, b.Age)
	})
	fmt.Println(people)

	slices.SortFunc(people, func(a, b Person) int {
		return cmp.Or(
			cmp.Compare(a.City, b.City),
			cmp.Compare(b.Age, a.Age),
			cmp.Compare(a.Name, b.Name),
		)
	})
	fmt.Println(people)

	scores := map[string]int{"ada": 3, "grace": 9, "linus": 5}
	for _, name := range slices.Sorted(maps.Keys(scores)) {
		fmt.Print(name, "=", scores[name], " ")
	}
	fmt.Println()

	idx, found := slices.BinarySearch(nums, 8)
	fmt.Println(idx, found)
}
Text
[2 2 5 8 9] true
[Banana Fig apple cherry]
[apple Banana cherry Fig]
[{linus 28 Boston} {ada 36 London} {alan 36 London} {grace 45 Boston}]
[{grace 45 Boston} {linus 28 Boston} {ada 36 London} {alan 36 London}]
ada=3 grace=9 linus=5 
3 true

The first block sorts ints in place with slices.Sort, and IsSorted confirms it. Strings sort by bytes, so sort.Strings put Banana and Fig before apple and cherry, because uppercase letters sort before lowercase. The next call fixes that with a comparison function that lowercases first, and SortFunc gets the case-insensitive order, apple Banana cherry Fig. This is the comparison style, "return negative, zero or positive", and cmp.Compare produces exactly that for any ordered type.

Sorting structs is the same call with a function that compares one field. Sorted by age, ada and alan tie at 36, and the output shows the order the tie came out in. That brings up stability, covered below. The second struct sort uses three keys. cmp.Or returns the first of its arguments that is not zero, so the comparison on city decides when the cities differ, the comparison with the arguments reversed, b.Age against a.Age, gives descending age within a city, and the name breaks any remaining tie. Reversing the arguments is the whole trick for descending order. The result groups Boston first and then London, with older people first in each group.

The last two lines sort a map by key, using the iterator helpers, and run a binary search on the sorted slice. BinarySearch returns the index where the value is, or where it would be inserted, and whether it was found. It requires a sorted slice and gives wrong answers silently if it is not.

Stability

A sort is stable if elements that compare equal keep the order they had before. slices.Sort and slices.SortFunc do not promise that, and the same input can come out with ties in a different order on a different Go version. If ties matter, as when you sort a table by one column that the user previously sorted by another, use slices.SortStableFunc, or add the tie-breaking key to the comparison as the three-key sort did, which is deterministic and is what I prefer. A stable sort exists to give you "sort by this, keeping the previous order among ties" without spelling the previous key out. It costs a little more time.

The older API, when you meet it

sort.Ints, sort.Strings and sort.Float64s are the one-line forms for the three basic types, and since Go 1.22 they call slices.Sort internally, so they cost the same. sort.Slice(s, func(i, j int) bool) sorts any slice with a "less" function that takes indexes. It uses reflection to swap elements, which is why it is slower, and the function closes over the slice, which is why people write bugs where the closure reads a different slice than the one being sorted. sort.Sort takes a value with Len, Less and Swap, which is the older interface pattern. New code should prefer slices.SortFunc, and old code that works can stay. The comparison returns an int in the new API and a bool in the old one: a.Age < b.Age becomes cmp.Compare(a.Age, b.Age).

What each costs

I sorted 100,000 random integers with four of the APIs, restoring the unsorted data before each run:

main_test.goGo
package main

import (
	"math/rand/v2"
	"slices"
	"sort"
	"testing"
)

const n = 100_000

func data() []int {
	r := rand.New(rand.NewPCG(1, 2))
	xs := make([]int, n)
	for i := range xs {
		xs[i] = r.IntN(1_000_000)
	}
	return xs
}

var src = data()

func BenchmarkSortInts(b *testing.B) {
	for b.Loop() {
		b.StopTimer()
		xs := slices.Clone(src)
		b.StartTimer()
		sort.Ints(xs)
	}
}

func BenchmarkSortSlice(b *testing.B) {
	for b.Loop() {
		b.StopTimer()
		xs := slices.Clone(src)
		b.StartTimer()
		sort.Slice(xs, func(i, j int) bool { return xs[i] < xs[j] })
	}
}

func BenchmarkSlicesSort(b *testing.B) {
	for b.Loop() {
		b.StopTimer()
		xs := slices.Clone(src)
		b.StartTimer()
		slices.Sort(xs)
	}
}

func BenchmarkSlicesSortFunc(b *testing.B) {
	for b.Loop() {
		b.StopTimer()
		xs := slices.Clone(src)
		b.StartTimer()
		slices.SortFunc(xs, func(a, b int) int { return a - b })
	}
}
Text
goos: darwin
goarch: arm64
pkg: example.test/lab
cpu: Apple M1 Max
BenchmarkSortInts-10          	     200	   5937344 ns/op	      30 B/op	       0 allocs/op
BenchmarkSortInts-10          	     202	   6375608 ns/op	       1 B/op	       0 allocs/op
BenchmarkSortInts-10          	     200	   5913335 ns/op	       2 B/op	       0 allocs/op
BenchmarkSortSlice-10         	     122	   9798090 ns/op	      61 B/op	       2 allocs/op
BenchmarkSortSlice-10         	     121	   9793940 ns/op	      62 B/op	       2 allocs/op
BenchmarkSortSlice-10         	     123	   9697081 ns/op	      58 B/op	       2 allocs/op
BenchmarkSlicesSort-10        	     207	   5851081 ns/op	       1 B/op	       0 allocs/op
BenchmarkSlicesSort-10        	     174	   6739543 ns/op	      32 B/op	       0 allocs/op
BenchmarkSlicesSort-10        	     205	   5876930 ns/op	       1 B/op	       0 allocs/op
BenchmarkSlicesSortFunc-10    	     150	   7902119 ns/op	       1 B/op	       0 allocs/op
BenchmarkSlicesSortFunc-10    	     146	   8098568 ns/op	       2 B/op	       0 allocs/op
BenchmarkSlicesSortFunc-10    	     150	   7931961 ns/op	       1 B/op	       0 allocs/op
PASS
ok  	example.test/lab	14.949s

slices.Sort and sort.Ints tied at about 5.8 ms, as they share an implementation. slices.SortFunc took about 8 ms, since every comparison is a function call, and sort.Slice took about 9.7 ms, roughly 70 percent slower than the specialized sort, from the reflection-based swap and the indirect "less" calls. The practical rule: use slices.Sort for basic types, SortFunc for structs, and do not bother optimizing a sort that takes milliseconds. The subtraction a - b in the benchmark's comparison is a shortcut that overflows for extreme values, and cmp.Compare is the correct form, which I would use in real code. These are single-machine numbers for one data size; a sort is O(n log n) for all of them and the ratios are what carry over.

Sorting by a value that is expensive to compute

A comparison function runs n log n times, so if it computes something costly for each element, such as lowercasing a string, parsing a date or hashing, it does that work many times per element. When the key is expensive, compute it once per element, sort pairs of the key and the element, and then drop the key. For lowercasing short strings the difference is negligible, and for parsing timestamps out of log lines or extracting a number from a file name it is worth the extra slice. Measure first, because a simple comparison with a cheap key is clearer and usually fast enough.

Pitfalls

Sorting a slice you did not clone changes the caller's data, since sorting is in place. A comparison function that is not a consistent ordering, for example one that uses <= or returns inconsistent signs, can make the sort panic or produce garbage. Floats with NaN need cmp.Compare, which puts NaN first, since < is not a total order for them. And sorting a very large slice of large structs moves the structs, so sort a slice of indexes or pointers instead if the elements are big, using the cost reasoning from ranging over large structs. When the data is already nearly sorted, the standard sort is adaptive, and when it is truly large and on disk, an external sort is a different problem. The documentation is at slices.SortFunc and cmp.Or.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.