The blind librarian problem
You have a slice of user records, log entries, or database rows. The default order is useless. You need to sort by timestamp, then by priority, then by name. Go gives you a single, highly optimized sorting algorithm in the standard library, but it refuses to guess what "less than" means for your custom data. You have to tell it.
The standard library solves this with a contract. The sorting algorithm does not care about your structs, your JSON tags, or your business logic. It only needs three answers: how many items exist, which of two items should come first, and a way to physically swap two items in memory. sort.Interface is that contract. It separates the mechanics of rearranging data from the rules that define order.
The three-question contract
sort.Interface lives in the sort package and requires exactly three methods. The names are strict. The signatures are strict. The compiler will reject anything that deviates.
type Interface interface {
// Len returns the number of elements in the collection.
Len() int
// Less reports whether the element at index i should sort before the element at index j.
Less(i, j int) bool
// Swap swaps the elements at indices i and j.
Swap(i, j int)
}
Think of the sorting algorithm as a blind librarian. The librarian knows exactly how to rearrange books on a shelf efficiently. The librarian does not care what is inside the books. All the librarian needs is three answers: how many books are there, which of two books should come first, and a way to physically swap two books on the shelf. sort.Interface is just that contract. It separates the how from the what.
Goroutines are cheap. Interfaces are implicit. You do not declare that your type implements sort.Interface. You just write the three methods. The compiler checks the method set at compile time and links them automatically.
Why you need a new type
You cannot attach methods directly to a built-in slice type like []Person. Go restricts method receivers to types defined in the same package. If you try to add Len, Less, or Swap directly to []Person, the compiler rejects the program with cannot define new methods on non-local type []Person.
The workaround is a type alias. You define a new type that is identical to your slice, then attach the methods to that new type.
package main
import (
"fmt"
"sort"
)
type Person struct {
Name string
Age int
}
// ByAge is a slice of Person sorted by Age.
// We create a new type so we can attach methods to it.
type ByAge []Person
// Len returns the number of people in the slice.
func (a ByAge) Len() int { return len(a) }
// Less compares two people by age.
// The receiver name 'a' matches the type initial, following Go convention.
func (a ByAge) Less(i, j int) bool {
return a[i].Age < a[j].Age
}
// Swap exchanges two people in the underlying slice.
// It modifies the original data in place.
func (a ByAge) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func main() {
people := []Person{{"Alice", 30}, {"Bob", 25}, {"Charlie", 28}}
// Cast the slice to ByAge so the methods are available.
sort.Sort(ByAge(people))
fmt.Println(people)
}
The cast ByAge(people) does not copy the data. It reinterprets the existing slice header. When Swap runs, it mutates the original people slice. Sorting in Go is always in-place unless you explicitly copy the data first.
Trust the type system. Wrap the slice or change the design.
How the algorithm calls your code
When you call sort.Sort, the standard library runs a hybrid algorithm. It starts with quicksort for speed, but switches to heapsort if the recursion depth gets too large. This prevents the pathological O(n²) worst case that plagues naive quicksort implementations. The algorithm repeatedly calls your Len, Less, and Swap methods until the collection satisfies the ordering contract.
The compiler verifies interface satisfaction before the program runs. If you misspell Less as less, or return int instead of bool, you get a compile-time error. The message is direct: ByAge does not implement sort.Interface (wrong type for Less method). There is no runtime penalty for interface checks. The method table is resolved statically.
The receiver naming convention matters here. Go developers typically use a one or two letter name that matches the type initial. (a ByAge), (s Students), (p People). You will rarely see (this ByAge) or (self ByAge). The convention keeps the code scannable and aligns with the standard library.
Real-world sorting: chaining comparisons
Single-field sorting is straightforward. Real data rarely is. You often need to sort by a primary key, then break ties with a secondary key, then a tertiary key. The Less method handles this with simple boolean logic.
type LogEntry struct {
Timestamp time.Time
Level string
Message string
}
type ByTimeThenLevel []LogEntry
func (s ByTimeThenLevel) Len() int { return len(s) }
func (s ByTimeThenLevel) Less(i, j int) bool {
// Compare timestamps first.
if !s[i].Timestamp.Equal(s[j].Timestamp) {
return s[i].Timestamp.Before(s[j].Timestamp)
}
// Timestamps are equal, so compare severity levels alphabetically.
return s[i].Level < s[j].Level
}
func (s ByTimeThenLevel) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
The Less method must be fast. The sorting algorithm calls it O(n log n) times. If you compute a hash, allocate memory, or make a network call inside Less, the sort will crawl. Precompute expensive values or cache them in the struct before sorting.
Stability matters when you sort multiple times. sort.Sort is not stable. If two elements compare as equal, their relative order may change. If you need to preserve the original order for equal elements, use sort.Stable instead. It uses a merge sort variant that guarantees stability at a slight performance cost.
The worst goroutine bug is the one that never logs. The worst sort bug is the one that silently reorders equal elements. Pick the algorithm that matches your data.
When the contract breaks
The sorting algorithm assumes your Less method defines a strict weak ordering. That means three mathematical properties must hold: irreflexivity, antisymmetry, and transitivity. If your comparison logic violates these, the algorithm will panic or return garbage.
Irreflexivity means Less(i, i) must always return false. An element cannot be less than itself. Antisymmetry means if Less(i, j) is true, then Less(j, i) must be false. Transitivity means if i < j and j < k, then i < k. Floating point comparisons with NaN break these rules. Custom string collations that ignore case but treat accents inconsistently break these rules.
The compiler cannot check mathematical properties. It only checks signatures. If you pass a broken Less function, the runtime will eventually hit an invariant violation and panic with panic: sort: Slice is not sorted or panic: runtime error: index out of range. The stack trace points to the sort package, not your code. Trace the panic back to your Less implementation and verify the ordering logic.
Modifying the slice length during a sort also causes immediate panics. The algorithm caches the length at the start. If Swap or Less appends to or slices the underlying array, the cached length becomes stale. The algorithm will index past the end of the slice. Keep the slice immutable during the sort. Create a new slice if you need to filter before sorting.
Goroutines are cheap. Channels are not magic. Sorting contracts are strict. Respect the math or the runtime will respect you back.
Pick the right tool
Go provides multiple ways to sort data. The standard library evolved from interface-based sorting to closure-based helpers, and finally to generic functions. Each has a specific use case.
Use sort.Interface when you need to sort a custom type repeatedly and want the comparison logic tightly coupled to the type definition. Use sort.Slice when you have a one-off sort on a built-in slice and prefer passing a closure instead of defining a new type. Use slices.SortFunc when you are on Go 1.21+ and want generic, type-safe sorting without boilerplate. Use plain sequential code when you don't need sorting: the simplest thing that works is usually the right thing.
The interface approach is verbose by design. The community accepts the boilerplate because it makes the comparison rules explicit and reusable. gofmt will format the method set consistently. Do not argue about indentation. Let the tool decide.