Go Sort and Custom Comparators: The Coding Interview Reference

May 29, 202610 min read
dsaalgorithmsinterview-prepcareer
Go Sort and Custom Comparators: The Coding Interview Reference
TL;DR
  • slices.SortFunc is the preferred API since Go 1.21 (LeetCode runs Go 1.22); sort.Slice still dominates older environments and is worth knowing
  • Never subtract in a comparator (a - b silently overflows when values span the full int range; use cmp.Compare(a, b) instead)
  • Multi-field sorting follows one pattern: check the first field, return early if they differ, fall through to the tiebreak
  • Stability only matters when sorting by a secondary key in separate passes or when original order must survive equal-key ties
  • sort.Interface (Len/Less/Swap) is the legacy API; know it exists but never reach for it when sort.Slice or slices.SortFunc is available
  • For primitive slices, sort.Ints / slices.Sort skip the comparator entirely; both use pdqsort under the hood

Go made sorting harder than it needed to be, then fixed it, then forgot to tell everyone. At version 1.21 the API split in two: the old sort.Slice is everywhere on LeetCode and in every Stack Overflow answer you'll find at midnight; the new slices.SortFunc is cleaner, type-safe, and what you should actually write. You need both from memory, because interview environments pick a runtime and don't tell you until you're already mid-function.

This guide covers every sorting pattern you'll reach for in Go under time pressure, plus the one bug that compiles perfectly, produces wrong output on large inputs, and makes you look like you don't test your code. You won't. Your interviewer won't either. That's the problem.


Which API Do You Actually Need?

APISinceComparator receivesReturnsType-safe?
sort.SliceGo 1.8two indices i, j intbool (is i before j?)No
sort.SliceStableGo 1.8two indices i, j intboolNo
sort.InterfaceGo 1(you implement Less)boolDepends
slices.SortFuncGo 1.21two elements a, b Tint (neg/zero/pos)Yes
slices.SortStableFuncGo 1.21two elements a, b TintYes

Reach for sort.Slice when the environment predates Go 1.21, and slices.SortFunc everywhere else. LeetCode updated its Go runtime to 1.22 in 2024, so slices.SortFunc is fair game for most interviews. Check the runtime version before you start. Not after you get a mysterious compile error.


sort.Slice Closures and the Reference Bug

sort.Slice takes the slice as an interface{} and a less function that receives two indices. The closure captures the outer slice by reference, which is elegant and also the exact mechanism behind one specific category of late-night debugging session.

import "sort" nums := []int{5, 2, 8, 1, 9} // ascending sort.Slice(nums, func(i, j int) bool { return nums[i] < nums[j] }) // descending sort.Slice(nums, func(i, j int) bool { return nums[i] > nums[j] })

Multi-field sorting follows the same shape. Sort by the most important key, fall through to tiebreaks:

type Person struct { Name string Age int } people := []Person{{"Alice", 30}, {"Bob", 25}, {"Carol", 35}} // age ascending, name as tiebreak sort.Slice(people, func(i, j int) bool { if people[i].Age != people[j].Age { return people[i].Age < people[j].Age } return people[i].Name < people[j].Name })

The closure-captures-the-outer-slice pattern means you can accidentally sort the wrong slice if you copy a reference incorrectly somewhere up the call stack. It compiles fine and produces garbage output at runtime. The compiler cannot catch it because the API is not generic. You're on your own.

This is the part where the slices package looks very good by comparison.


slices.SortFunc: Write It This Way Instead

The slices package, standard since Go 1.21, gives you a fully generic sort. The comparator receives actual elements (not indices) and returns an int following the Unix convention: negative if a < b, zero if equal, positive if a > b. Same contract as C's strcmp, which has been the right answer since 1972.

import ( "cmp" "slices" ) nums := []int{5, 2, 8, 1, 9} // ascending slices.SortFunc(nums, func(a, b int) int { return cmp.Compare(a, b) }) // descending: swap a and b slices.SortFunc(nums, func(a, b int) int { return cmp.Compare(b, a) })

cmp.Compare handles any ordered type correctly. For structs, you write the comparison yourself, but the shape is always the same:

slices.SortFunc(people, func(a, b Person) int { if n := cmp.Compare(a.Age, b.Age); n != 0 { return n } return cmp.Compare(a.Name, b.Name) })

Check the first field, early-return if they differ, fall through to the tiebreak. That is the idiomatic multi-field pattern regardless of how many sort keys you have. Identical shape every time. Memorize it once.


The Bug That Fails Candidates Silently

Every language with custom comparators has this trap. Go is no exception. It is the bug most commonly written under interview pressure, because it looks correct, is fast to type, and explodes only on edge cases you didn't think to test.

// WRONG: integer overflow when values span the full int64 range slices.SortFunc(nums, func(a, b int) int { return a - b })

This looks fine. On typical small inputs it works. If a = math.MaxInt64 and b = -1, then a - b overflows to a large negative number, your comparator returns the wrong sign, and your sort output is quietly incorrect. No panic. No error. Just wrong answers on the test cases that involve large values.

Never use subtraction as a comparator. Use cmp.Compare(a, b) or an explicit if/else.

// Option 1: cmp.Compare (preferred) slices.SortFunc(nums, func(a, b int) int { return cmp.Compare(a, b) }) // Option 2: explicit branches (more typing, impossible to misread) slices.SortFunc(nums, func(a, b int) int { if a < b { return -1 } if a > b { return 1 } return 0 })

The same trap bites Java and JavaScript candidates. Java's Integer.compare(a, b) and Go's cmp.Compare(a, b) exist precisely to be the default safe path. If you use both languages in interviews, see Go vs Java for coding interviews for a side-by-side on where each sort API goes sideways.


When Stability Changes the Output

sort.Slice and slices.SortFunc are both unstable. Equal elements can land in any order relative to each other. Usually this is fine. It matters when:

  1. You're sorting by a secondary key in a separate pass (radix-style multi-pass sorting).
  2. The problem explicitly requires preserving the original order of ties.

See how stable sort actually works for a deeper treatment of why stability requires extra memory and when the trade is worth it. The stable variants are drop-in replacements with identical signatures:

sort.SliceStable(people, func(i, j int) bool { return people[i].Age < people[j].Age }) slices.SortStableFunc(people, func(a, b Person) int { return cmp.Compare(a.Age, b.Age) })

In most interview problems you're sorting once on a compound key. Put all tiebreakers in a single comparator and you never need the stable variant. The stable path exists for the cases where original order is part of the problem's correctness condition, not just a nicety.


sort.Interface: Know It, Don't Reach for It

Before sort.Slice, the only way to sort custom types in Go was implementing three methods on a named type. This is the original API from Go 1.0. It is still valid Go. Your interviewer might ask what it looks like, and you should be able to produce it without panicking.

type ByAge []Person func (a ByAge) Len() int { return len(a) } func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age } func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] } sort.Sort(ByAge(people))

Three separate method declarations to sort one slice by one field. It works. It is not what you want to write when you have 45 minutes and a comparator is one step in a larger algorithm.

For actually solving problems, sort.Slice or slices.SortFunc is the right choice. Know that sort.Interface exists; reach for it only when you need to attach sort behavior to an existing named type for reuse. For a broader look at Go's standard library in interview context, see the Go for coding interviews cheat sheet.


Sorting and Binary Search Together

Sorting and searching are almost always paired. You sort a list of intervals, then binary-search for the insertion point. You sort candidates, then binary-search for the feasibility boundary. Go's sort.Search finds the smallest index where a predicate becomes true.

import "sort" nums := []int{1, 3, 5, 7, 9, 11} // convenience wrapper: first index >= target idx := sort.SearchInts(nums, 7) // idx = 3 // general form target := 7 idx = sort.Search(len(nums), func(i int) bool { return nums[i] >= target })

sort.SearchInts, sort.SearchFloat64s, and sort.SearchStrings cover the common cases. The predicate must be monotone: false below some threshold, true at and above it. This is the same invariant behind every binary search implementation, just expressed as a function instead of a loop.

The modern equivalent from the slices package:

// returns (index, found bool) idx, found := slices.BinarySearchFunc(people, Person{Age: 30}, func(p, target Person) int { return cmp.Compare(p.Age, target.Age) })

For Primitives, Skip the Comparator Entirely

You do not need a custom comparator for a flat slice of ints or strings. Go has convenience wrappers that are shorter to type and harder to mess up:

nums := []int{5, 2, 8, 1} sort.Ints(nums) strs := []string{"banana", "apple", "cherry"} sort.Strings(strs) floats := []float64{3.14, 1.41, 2.71} sort.Float64s(floats) // Go 1.21+: works for any ordered type slices.Sort(nums)

Both families are O(n log n) with pdqsort under the hood. The slices package avoids interface boxing via generics, which makes it marginally faster for value types. In an interview that margin will not matter. What will matter is whether you burned 90 seconds writing a closure for a problem that needed sort.Ints.


Writing the Comparator Out Loud

Sorting shows up in interviews not as the problem itself, but as one step inside something bigger: normalize intervals before merging them, rank candidates for a greedy pass, sort by frequency before constructing output. The sort is one move in a longer sequence, and fumbling it burns time you can't recover.

The failure mode here is specific. Under pressure, you write a comparator that looks right, is subtly inconsistent (maybe you flipped the comparison on the tiebreak), and produces output that fails on inputs you didn't trace. By the time you spot it, you've lost several minutes and your interviewer has noted the debugging spiral.

Writing the comparator correctly the first time, and narrating what you're sorting and why as you write it, is the difference between solving the problem and nearly solving it. That narration is also one of the signals that shows up in the hidden rubric for communication: explaining what your comparator is doing before the interviewer has to ask.

Training that full loop, including the spoken explanation, is what SpaceComplexity is built for. Voice-based mock interviews where the rubric scores whether your explanation matched your code.


Quick Reference

// Primitive slice sort.Ints(nums) slices.Sort(nums) // Go 1.21+ // Custom comparator, classic sort.Slice(s, func(i, j int) bool { return s[i].Field < s[j].Field }) // Custom comparator, modern (preferred) slices.SortFunc(s, func(a, b T) int { return cmp.Compare(a.Field, b.Field) }) // Descending slices.SortFunc(nums, func(a, b int) int { return cmp.Compare(b, a) }) // Multi-field slices.SortFunc(s, func(a, b T) int { if n := cmp.Compare(a.Primary, b.Primary); n != 0 { return n } return cmp.Compare(a.Secondary, b.Secondary) }) // Stable sort.SliceStable(s, func(i, j int) bool { return s[i].Field < s[j].Field }) slices.SortStableFunc(s, func(a, b T) int { return cmp.Compare(a.Field, b.Field) }) // Binary search sort.SearchInts(nums, target) // first index >= target slices.BinarySearch(nums, target) // (index, found), Go 1.21+

Further Reading