How to Sort Custom Types in Go

Implement Len, Less, and Swap methods on your type to satisfy sort.Interface, then call sort.Sort to order your custom data.

Sorting custom types in Go

You have a slice of structs representing database rows, API responses, or game entities. The data arrives in random order. You need it sorted by name, then by score, then by timestamp. Python offers sorted(list, key=...). JavaScript provides .sort((a, b) => ...). Go takes a different approach that separates the data from the comparison logic. This separation makes sorting explicit, composable, and efficient.

Go does not have a generic sort function that accepts a key extractor. Instead, the standard library defines a contract. You implement the contract on your data, and the sort package handles the algorithm. This pattern feels more verbose at first, but it scales well when you need multiple sort orders or high performance.

The sort interface contract

The sort package relies on sort.Interface. This interface requires three methods: Len, Less, and Swap. The sort package does not care about your struct fields. It only cares that you can answer three questions: how many items are there, which item comes first, and how to exchange two items.

Think of it like a library reorganization system. The system doesn't know what books are. It just needs to know the total count, how to compare two books to see which belongs on the left, and how to physically swap them on the shelf. You provide the logic; the system provides the movement.

The convention for receiver naming is one or two letters matching the type. Use (s ByValue) not (this ByValue). The type name usually describes the sort criteria. ByValue, ByName, or ByPriority make the intent clear to anyone reading the code.

Minimal implementation

Here's the pattern: define a type alias for the slice, implement the three methods, and pass it to sort.Sort. The type alias adds the methods without copying the underlying data.

// Item holds a name and a numeric value.
type Item struct {
	Name  string
	Value int
}

// ByValue implements sort.Interface for []Item based on Value.
// The type name describes the sort criteria.
type ByValue []Item

// Len returns the number of items.
// The sort package calls this to determine slice bounds.
func (s ByValue) Len() int {
	return len(s)
}

// Less reports whether the item at index i should sort before j.
// This is the only method that contains your business logic.
func (s ByValue) Less(i, j int) bool {
	return s[i].Value < s[j].Value
}

// Swap exchanges the items at indices i and j.
// The sort package calls this to reorder the slice.
func (s ByValue) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}

The implementation is straightforward. Len delegates to the built-in len. Swap uses Go's multiple assignment to exchange elements. Less contains the comparison. The cast to ByValue is zero-cost. It creates a new slice header that points to the same underlying array, but now the header includes the method set.

func main() {
	items := []Item{
		{"A", 2},
		{"B", 1},
		{"C", 3},
	}

	// Cast to ByValue to expose the sort methods.
	// The cast is zero-cost; it points to the same slice.
	sort.Sort(ByValue(items))

	// items is now sorted in place.
	// Result: [{A 1} {B 2} {C 3}]
}

The cast is free. The methods are cheap. The sort is in place.

How the sort runs

When you call sort.Sort, the package checks that the argument implements sort.Interface. It then runs a sorting algorithm, typically a variation of quicksort or heapsort depending on the data characteristics. The algorithm calls Len to know the range, Less to compare elements, and Swap to move them.

The sort modifies the slice in place. No new slice is allocated. The original variable holds the sorted data after the call returns. This is efficient for large datasets.

The sort package does not guarantee stability. If two elements compare as equal, their relative order after sorting is undefined. If you need to preserve the original order of equal elements, use sort.Stable instead. sort.Stable uses a different algorithm that guarantees stability at a slight performance cost.

The interface is a contract. Implement the three methods, and the sort package does the rest.

Realistic multi-key sorting

Real applications often need complex sorting rules. You might sort by priority descending, then by name ascending. The Less method handles this hierarchy. You compare the primary key first. If they differ, you return the result. If they are equal, you fall through to the secondary key.

// ByPriorityAndName sorts by Priority descending, then Name ascending.
type ByPriorityAndName []Task

func (s ByPriorityAndName) Len() int { return len(s) }
func (s ByPriorityAndName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s ByPriorityAndName) Less(i, j int) bool {
	// Compare priority first.
	// Higher priority values come first.
	if s[i].Priority != s[j].Priority {
		return s[i].Priority > s[j].Priority
	}
	// Tie-break by name alphabetically.
	return s[i].Name < s[j].Name
}

This pattern scales to any number of keys. Add more if blocks or use a helper function to keep Less readable. The comparison logic must be pure and fast. The sort algorithm calls Less many times. Expensive operations inside Less will slow down the entire sort.

Complex sorting lives in Less. Keep the comparison logic pure and fast.

Modern alternative: slices package

Go 1.21 introduced the slices package. slices.SortFunc provides a more concise way to sort slices without defining a new type. It takes the slice and a comparison function that returns -1, 0, or 1. This is often easier for one-off sorts where you don't need reusable sorters.

import "slices"

func sortWithSlices(items []Item) {
	// SortFunc takes the slice and a comparison function.
	// The function returns -1, 0, or 1.
	slices.SortFunc(items, func(a, b Item) int {
		if a.Value < b.Value {
			return -1
		}
		if a.Value > b.Value {
			return 1
		}
		return 0
	})
}

slices.SortFunc is idiomatic for simple cases. It avoids the boilerplate of defining a type and three methods. The comparison function captures the logic directly. For multi-key sorts, you can still write the same hierarchy inside the function.

slices.SortFunc trades a tiny bit of abstraction for readability. Use it when the sort logic is simple and local.

Pitfalls and compiler errors

Sorting custom types has a few common traps. The compiler catches some, but others manifest as runtime bugs.

If you define the interface methods with pointer receivers, the sort package rejects the type. sort.Interface requires value receivers. The compiler complains with *ByValue does not implement sort.Interface (method Less has pointer receiver). Fix this by changing the receiver to a value type.

// BAD: Pointer receiver fails the interface check.
func (s *ByValue) Less(i, j int) bool { ... }

// GOOD: Value receiver satisfies the interface.
func (s ByValue) Less(i, j int) bool { ... }

Forgetting the cast is another common error. If you pass the raw slice to sort.Sort, the compiler rejects it with []Item does not implement sort.Interface (missing Len method). The slice type itself has no methods. You must cast it to the type that implements the interface.

The Less function must be transitive. If a < b and b < c, then a < c must hold. If your comparison logic violates transitivity, the sort may panic, loop forever, or produce incorrect results. This is a runtime bug. The compiler cannot check transitivity. Test your comparison logic carefully.

Using sort.Sort when you need stability is a subtle bug. If you sort by date, then by ID, and use an unstable sort, the ID order may get scrambled for items with the same date. Use sort.Stable or slices.SortStableFunc when relative order matters.

Transitivity is the law. If A < B and B < C, then A < C must hold, or the sort will break.

Decision matrix

Go offers multiple ways to sort. Pick the right tool based on your needs.

Use slices.SortFunc when you need a quick sort with a simple comparison function and are on Go 1.21 or later.

Use sort.Interface when you need multiple sort orders on the same type or are sorting in a performance-critical loop where avoiding closure allocation matters.

Use sort.Slice when you are on an older Go version and need a one-off sort without defining a new type.

Use sort.Stable when equal elements must preserve their original relative order.

Pick the tool that matches the frequency. One-off gets SortFunc. Reusable gets Interface.

Where to go next