56. Sorting and searching
Sorting comes up constantly — and in Go it's almost always one line. This
lesson covers the sort package you'll see in every codebase, plus the newer
generic slices package that's replacing it.
The one-liners
package main
import (
"fmt"
"sort"
)
func main() {
nums := []int{5, 2, 8, 1, 9}
words := []string{"banana", "apple", "cherry"}
prices := []float64{9.99, 2.50, 19.95}
sort.Ints(nums)
sort.Strings(words)
sort.Float64s(prices)
fmt.Println(nums)
fmt.Println(words)
fmt.Println(prices)
fmt.Println(sort.IntsAreSorted(nums))
}
All three sort in place — they modify the slice and return nothing. If you need the original order preserved, copy first (module 4).
sort.Slice — sort anything by anything
The workhorse. You supply a less function and it does the rest:
package main
import (
"fmt"
"sort"
)
type Person struct {
Name string
Age int
City string
}
func main() {
people := []Person{
{"Ada", 36, "London"},
{"Grace", 45, "Arlington"},
{"Alan", 41, "London"},
{"Katherine", 45, "Hampton"},
}
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})
fmt.Println("by age:")
for _, p := range people {
fmt.Printf(" %-10s %d\n", p.Name, p.Age)
}
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
})
fmt.Println("by age desc, then name:")
for _, p := range people {
fmt.Printf(" %-10s %d\n", p.Name, p.Age)
}
}
The less(i, j) function answers one question: should element i come
before element j? Everything follows from that:
- ascending:
a[i] < a[j] - descending:
a[i] > a[j] - multi-key: compare the primary key, and only fall through to the secondary when the primary is equal — exactly the shape in the second sort above.
Stable vs unstable
package main
import (
"fmt"
"sort"
)
type Entry struct {
Group string
Order int
}
func main() {
entries := []Entry{
{"b", 1}, {"a", 2}, {"b", 3}, {"a", 4}, {"c", 5},
}
sort.SliceStable(entries, func(i, j int) bool {
return entries[i].Group < entries[j].Group
})
for _, e := range entries {
fmt.Printf("%s%d ", e.Group, e.Order)
}
fmt.Println()
fmt.Println("within each group, original order is preserved")
}
sort.Slice uses an introsort variant that is not stable — equal
elements can be reordered. sort.SliceStable preserves their relative order,
at a small cost.
Use SliceStable when you sort by one key after already sorting by another,
which is how you build multi-level sorts incrementally.
Sorting map contents
Maps have no order, so the recipe from module 4 shows up again:
package main
import (
"fmt"
"sort"
)
func main() {
votes := map[string]int{
"go": 42, "rust": 37, "zig": 12, "c": 42,
}
names := make([]string, 0, len(votes))
for name := range votes {
names = append(names, name)
}
sort.Slice(names, func(i, j int) bool {
if votes[names[i]] != votes[names[j]] {
return votes[names[i]] > votes[names[j]]
}
return names[i] < names[j]
})
for i, name := range names {
fmt.Printf("%d. %-5s %d\n", i+1, name, votes[name])
}
}
Collect keys → sort them with a comparator that looks up the map → iterate.
Note the tie-break on name: without it, go and c (both 42) would come out
in an arbitrary order, and your output would differ between runs.
Always break ties on something deterministic when the output is a report, a test fixture, or anything a human will diff.
Binary search
Once a slice is sorted, sort.Search finds things in O(log n):
package main
import (
"fmt"
"sort"
)
func main() {
nums := []int{1, 3, 5, 7, 9, 11}
i := sort.SearchInts(nums, 7)
fmt.Println("index of 7:", i)
j := sort.SearchInts(nums, 8)
fmt.Println("8 would be inserted at:", j, "- present:", j < len(nums) && nums[j] == 8)
k := sort.Search(len(nums), func(i int) bool { return nums[i] >= 6 })
fmt.Println("first element >= 6 is at index", k, "->", nums[k])
}
sort.Search is more general than it first looks: it returns the smallest
index for which the function is true, assuming the function goes false →
true exactly once across the slice. That makes it a tool for any monotonic
predicate, not just equality.
Note the "present" check: search functions return an insertion point, so you must verify the element at that index is actually what you wanted.
sort.Interface, for completeness
The original mechanism, from module 6:
package main
import (
"fmt"
"sort"
)
type ByLength []string
func (s ByLength) Len() int { return len(s) }
func (s ByLength) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s ByLength) Less(i, j int) bool { return len(s[i]) < len(s[j]) }
func main() {
words := []string{"banana", "fig", "apple", "kiwi"}
sort.Sort(ByLength(words))
fmt.Println(words)
sort.Sort(sort.Reverse(ByLength(words)))
fmt.Println(words)
}
Three methods and you can sort with sort.Sort. The payoff is
sort.Reverse, which wraps any sort.Interface and flips its Less — a
neat demonstration of interfaces composing.
For new code, sort.Slice with an inline function is shorter and just as
fast. Learn sort.Interface because you'll read it.
The modern way: slices
Go 1.21 added generic versions that are shorter still:
import "slices"
nums := []int{5, 2, 8}
slices.Sort(nums) // [2 5 8], no sort.Ints needed
people := []Person{...}
slices.SortFunc(people, func(a, b Person) int {
return cmp.Compare(a.Age, b.Age) // -1, 0 or +1
})
i, found := slices.BinarySearch(nums, 5)
slices.Contains(nums, 8)
slices.Index(nums, 2)
slices.Reverse(nums)
slices.Max(nums)
Two differences worth noting: SortFunc takes a three-way comparison
(negative / zero / positive, like C's strcmp) rather than a boolean less,
and BinarySearch returns (index, found) so you don't have to check
yourself.
New code should prefer slices. sort isn't deprecated and isn't going
anywhere — you'll see both for years.
(That box is reference-only: the slices package is newer than the
interpreter running these lessons.)
Your turn
Sort the products by price descending, breaking ties by name ascending, and print them:
Chair 199.50
Monitor 199.50
Keyboard 49.99
package main
import (
"fmt"
"sort"
)
type Product struct {
Name string
Price float64
}
func main() {
products := []Product{
{"Keyboard", 49.99},
{"Monitor", 199.50},
{"Chair", 199.50},
}
// sort by price descending, then name ascending
for _, p := range products {
fmt.Printf("%s %.2f\n", p.Name, p.Price)
}
}
package main
import (
"fmt"
"sort"
)
type Product struct {
Name string
Price float64
}
func main() {
products := []Product{
{"Keyboard", 49.99},
{"Monitor", 199.50},
{"Chair", 199.50},
}
sort.Slice(products, func(i, j int) bool {
if products[i].Price != products[j].Price {
return products[i].Price > products[j].Price
}
return products[i].Name < products[j].Name
})
for _, p := range products {
fmt.Printf("%s %.2f\n", p.Name, p.Price)
}
}
Chair and Monitor are tied on price, so the second clause decides, and
C sorts before M. Being able to predict a comparator's output exactly —
including the ties — is the skill this lesson is really teaching.
Next: reading and writing streams.