Complete Guide to the sort Package in Go

The sort package in Go provides functions to sort slices of basic types and custom data using comparison functions.

Sorting data in Go

You're building a leaderboard. The data arrives as a messy slice of structs with scores and timestamps. You need the highest scores at the top, and ties broken by earliest timestamp. Or you're parsing a CSV export where rows must be ordered by date before you can diff them against yesterday's report. Sorting is one of those tasks that feels trivial until you realize you have to write the comparison logic yourself, or you're dealing with a type the standard library doesn't know about.

The sort package handles this with a clean split. For basic types like []int or []string, there are direct functions that work instantly. For custom types, you use sort.Slice with a comparison function. The package also provides tools for binary search and performance-critical sorting patterns.

Basic types and custom slices

The sort package exports functions for common slice types. sort.Ints, sort.Float64s, and sort.Strings sort their respective slices in ascending order. These functions are fast and require zero setup.

When your data is a slice of structs or pointers, you use sort.Slice. You pass the slice and a function that compares two elements. The function receives two indices and must return true if the element at the first index should come before the element at the second index.

package main

import (
	"fmt"
	"sort"
)

// Person holds basic user data.
type Person struct {
	Name string
	Age  int
}

func main() {
	// Start with unsorted data.
	people := []Person{
		{Name: "Alice", Age: 30},
		{Name: "Bob", Age: 25},
		{Name: "Charlie", Age: 35},
	}

	// sort.Slice reorders the slice in place.
	// The closure defines the sort order: younger ages come first.
	sort.Slice(people, func(i, j int) bool {
		return people[i].Age < people[j].Age
	})

	// Print the result to verify the order.
	fmt.Println(people)
}

The closure captures the people slice and uses the indices to access elements. The sort algorithm calls your function repeatedly, swapping elements until the order satisfies the comparison. The slice is modified in place. There is no return value.

How sort.Slice works

sort.Slice uses a comparison-based algorithm. It treats your closure as the source of truth for ordering. The function signature is func(i, j int) bool. The runtime passes indices i and j and expects a boolean. Return true if i precedes j. Return false otherwise.

The algorithm does not copy your data. It swaps elements within the original slice. This keeps memory usage low. The closure is allocated on the heap if it captures variables, which adds a small allocation cost per call. For most applications, this cost is negligible. If you are sorting in a tight loop millions of times, the allocation adds up. In that case, sort.Interface offers a zero-allocation alternative.

Stability matters. A stable sort preserves the relative order of elements that compare as equal. sort.Slice is not guaranteed to be stable. If you need stability, use sort.SliceStable. The performance difference is small, but the guarantee is essential for multi-key sorting.

Stable sort saves your multi-key logic. Unstable sort scrambles ties.

Multi-key sorting and stability

Real data rarely has just one dimension. You often need to sort by age, but break ties alphabetically by name. Because sort.Slice is unstable, chaining sorts can produce unpredictable results. The second sort might reorder elements that have equal ages, destroying the name order established by the first sort.

sort.SliceStable solves this. It guarantees that equal elements retain their original relative order. You can sort by the least important key first, then sort by the most important key. The stable sort preserves the secondary order when the primary keys are equal.

// SortPeopleByAgeThenName sorts the slice in place.
// It sorts by age ascending, then by name ascending for ties.
func SortPeopleByAgeThenName(people []Person) {
	// Sort by name first.
	// This establishes the secondary order.
	sort.SliceStable(people, func(i, j int) bool {
		return people[i].Name < people[j].Name
	})

	// Sort by age second.
	// The stable sort preserves the name order when ages are equal.
	sort.SliceStable(people, func(i, j int) bool {
		return people[i].Age < people[j].Age
	})
}

The first pass orders by name. The second pass orders by age. When two people have the same age, the stable sort keeps them in the name order from the first pass. This pattern generalizes to any number of keys. Sort from least significant to most significant.

Binary search with sort.Search

The sort package includes sort.Search, which performs a binary search on a sorted slice. This function is often misunderstood. It does not search for a value. It searches for a condition boundary.

sort.Search takes the length of the slice and a function f func(int) bool. It finds the smallest index i where f(i) is true. It assumes the function is monotonic: false for low indices and true for high indices. If the function is not monotonic, the result is undefined.

This pattern is useful for finding insertion points or checking membership in large sorted datasets.

// FindInsertionPoint returns the index where target should be inserted.
// The slice must be sorted in ascending order.
func FindInsertionPoint(data []int, target int) int {
	// sort.Search finds the first index where data[i] >= target.
	// The predicate returns true when the element is greater or equal.
	return sort.Search(len(data), func(i int) bool {
		return data[i] >= target
	})
}

If target exists in the slice, FindInsertionPoint returns its index. If target is not present, it returns the index where it would be inserted to maintain order. If all elements are less than target, it returns len(data).

Binary search requires sorted data. Garbage in, garbage out.

Performance and sort.Interface

sort.Slice allocates a closure. In performance-critical code, you can avoid this allocation by implementing sort.Interface. The interface requires three methods: Len, Less, and Swap. You define these methods on a type that wraps your slice.

This approach moves the comparison logic into methods, eliminating the closure. The sort package calls the methods directly. This reduces GC pressure in hot paths.

// ByAge implements sort.Interface for []Person based on Age.
type ByAge []Person

// Len returns the number of elements.
func (a ByAge) Len() int {
	return len(a)
}

// Less reports whether element i should sort before element j.
func (a ByAge) Less(i, j int) bool {
	return a[i].Age < a[j].Age
}

// Swap exchanges elements i and j.
func (a ByAge) Swap(i, j int) {
	a[i], a[j] = a[j], a[i]
}

// SortByAge sorts people in place using the interface.
func SortByAge(people []Person) {
	// Convert the slice to the interface type.
	// sort.Sort uses the methods on ByAge.
	sort.Sort(ByAge(people))
}

The receiver name is a, matching the type abbreviation. This follows Go convention for short receiver names. The methods are called directly by the sort algorithm. No closure is created.

If you need stability with sort.Interface, use sort.Stable instead of sort.Sort.

Convention aside: receiver names for sort.Interface implementations are usually one or two letters matching the type, like (a ByAge). Avoid this or self. The community expects short names.

Pitfalls and errors

The comparison function must be consistent. If less(i, j) returns true and less(j, i) also returns true, the sort panics with panic: sorting algorithm is inconsistent. This usually happens when you use <= instead of < in the comparison. The function must define a strict weak ordering.

The compiler enforces the return type. If you forget to return a boolean, you get cannot use func literal (type func(int, int)) as type func(int, int) bool in argument to sort.Slice. The closure must return bool.

Accessing out of bounds panics. The sort algorithm passes valid indices, but if your closure accesses a different slice or modifies indices, you can trigger panic: runtime error: index out of range. Always use the indices provided by the sort function.

sort.Search requires a monotonic predicate. If your predicate oscillates, the result is undefined. The function does not validate monotonicity. It assumes you know the data is sorted and the predicate matches the sort order.

The worst goroutine bug is the one that never logs. The worst sort bug is the one that silently produces wrong order because you used sort.Slice instead of sort.SliceStable.

When to use what

Use sort.Ints when you have a slice of integers and need a quick ascending sort.

Use sort.Strings when you have a slice of strings and need lexicographic ordering.

Use sort.Slice when you need to sort a custom type and stability doesn't matter.

Use sort.SliceStable when you need to sort by multiple keys or preserve the original order of equal elements.

Use sort.Search when you need to find an element or insertion point in a sorted slice efficiently.

Use sort.Interface when you are sorting the same type repeatedly in performance-critical code and want to avoid closure allocation overhead.

Use sort.Reverse when you need descending order for a type that already implements sort.Interface.

Trust the stable sort for multi-key logic. Reach for the interface when the profiler points at allocations.

Where to go next