24.6 sort.Slice and sort.Stable: Sorting with Less Functions
Right, let’s talk about sorting slices. You’ve probably tried to sort a []int with sort.Ints and it was blissfully simple. But then you tried to sort a slice of your own structs—a []Person by last name, maybe—and you hit a wall. The built-in sort package doesn’t know your Person from a hole in the ground. That’s where sort.Slice and sort.SliceStable come in. They are your gateway to sorting anything, and they do it by making you do the heavy lifting. It’s the “I’ll hold the light, you fix the engine” approach to sorting.
The core of it is the less function. You provide a function that, given two indices i and j, reports whether the element at index i should come before the element at index j in the sorted slice. The sorting algorithm will call this function, over and over, to figure out the correct order. Your job is to tell it the truth.
people := []Person{
{"Weir", "Andy"},
{"Doe", "John"},
{"Smith", "Jane"},
}
// Sort by LastName
sort.Slice(people, func(i, j int) bool {
return people[i].LastName < people[j].LastName
})
fmt.Println(people) // [{Doe John} {Smith Jane} {Weir Andy}]
The Difference Between Slice and SliceStable
This is the first thing you need to get right, and the choice is usually straightforward. sort.Slice uses a fast, efficient, but unstable sort (the default is a quicksort variant). sort.SliceStable uses a slower, but stable sort.
“Stable” is a fancy way of saying it preserves the original order of elements that are considered equal. Why does that matter? Imagine you first sort your people slice by FirstName:
// Sort by FirstName first
sort.SliceStable(people, func(i, j int) bool {
return people[i].FirstName < people[j].FirstName
})
// Now people is: [{Weir Andy}, {Smith Jane}, {Doe John}]
Now, let’s sort by LastName. If you use sort.Slice, the algorithm doesn’t care about the previous order. Any two people with the same last name (which we don’t have in this example, but imagine if we did) could be rearranged arbitrarily. But if you use sort.SliceStable, and two people have the same last name, their relative order—which was already sorted by first name—is maintained. The first sort isn’t obliterated.
// Now sort by LastName, but keep the FirstName order for ties
sort.SliceStable(people, func(i, j int) bool {
return people[i].LastName < people[j].LastName
})
// The stable sort preserves the previous (FirstName) order for equal LastNames.
Rule of Thumb: Use sort.Slice for raw speed when the original order of “equal” elements is meaningless. Use sort.SliceStable when you’re doing multiple passes of sorting (e.g., sort by department, then by salary) or when you need to preserve that order for some other reason. When in doubt, Stable is a safe, slightly more expensive bet.
The Perils of Your Less Function
Your less function is law. The sort algorithms trust it completely, which means you can easily shoot yourself in the foot. Here are the two biggest ways people mess it up.
First, your function must be consistent. If less(i, j) returns true, then less(j, i) must return false. If your function breaks this transitive property, the sort will panic, or worse, produce garbage. This often happens with faulty floating-point comparisons or broken logic.
// DANGER: Bad floating-point comparison for equality.
// This does NOT establish a consistent ordering.
points := []float64{math.NaN(), 1.2, 2.4}
sort.Slice(points, func(i, j int) bool {
// This is wrong! NaN is not less than, greater than, or equal to anything.
return points[i] < points[j]
}) // This will likely panic.
Second, beware of capturing the slice in a closure. This sounds weird, but look at this common refactoring mistake:
// Let's abstract the comparison logic! (Famous last words)
type Person struct {
LastName string
FirstName string
}
byLastName := func(i, j int) bool {
// This captures the 'people' slice from the outer scope.
return people[i].LastName < people[j].LastName
}
sort.Slice(people, byLastName) // This works... for now.
What if you change the variable people to point to a different slice? Or what if you want to reuse byLastName for a different slice? You can’t. This closure is hard-wired to the original people variable. The better pattern is to write a comparator that takes the slice as an argument.
// This is a much more reusable and less error-prone pattern.
func SortPeopleByLastName(people []Person) {
less := func(slice []Person, i, j int) bool {
return slice[i].LastName < slice[j].LastName
}
// We have to use a wrapper because sort.Slice expects a func(i, j int) bool
sort.Slice(people, func(i, j int) bool {
return less(people, i, j)
})
}
It’s a few more lines, but it’s explicit and avoids tricky closure capture issues.
Sorting in Reverse
Need to sort descending? Don’t look for a sort.SliceReverse function. There isn’t one. The designers decided, correctly, that it’s trivial to implement in your less function. Just reverse the comparison.
Want to sort by LastName descending?
sort.Slice(people, func(i, j int) bool {
// Greater than instead of less than = descending order.
return people[i].LastName > people[j].LastName
})
It’s almost too simple. The beauty is that it works for any criteria. It’s a single place to control the entire ordering logic.