Sort #
Sorting is a fundamental operation that appears everywhere — displaying product lists by price, ordering logs by time, searching sorted data, or determining the largest and smallest elements. The sort package in Go provides everything needed: sorting for primitive types, sorting slices with custom comparators, stable sorting to preserve the order of equal elements, and binary search for finding values in sorted data. Go 1.21 also introduced the slices package, which provides more modern and ergonomic generic sorting functions. Understanding both gives you the right tool for every sorting scenario.
Overview #
flowchart TD
S["Sorting in Go"] --> Sort["package sort\n(pre-Go 1.21)"]
S --> Slices["package slices\n(Go 1.21+, generic)"]
Sort --> SP["sort.Ints\nsort.Strings\nsort.Float64s\n(primitive types)"]
Sort --> SS["sort.Slice\nsort.SliceStable\n(slice with a comparator)"]
Sort --> SI["sort.Sort\nsort.Stable\n(Len/Less/Swap interface)"]
Sort --> SB["sort.Search\n(binary search)"]
Sort --> SC["sort.IsSorted\nsort.Reverse\n(utilities)"]
Slices --> SL1["slices.Sort\nslices.SortFunc\n(generic, faster)"]
Slices --> SL2["slices.SortStableFunc\nslices.IsSorted\nslices.IsSortedFunc"]
Slices --> SL3["slices.BinarySearch\nslices.BinarySearchFunc"]
Slices --> SL4["slices.Min / slices.Max\nslices.MinFunc / slices.MaxFunc"]
style S fill:#4f86c6,color:#fff
style Sort fill:#e8f5e9
style Slices fill:#e3f2fdSorting Primitive Types #
For slices of primitive types, sort provides direct functions without needing to define a comparator:
package main
import (
"fmt"
"sort"
)
func main() {
// Integers
numbers := []int{5, 2, 8, 1, 9, 3, 7, 4, 6}
sort.Ints(numbers)
fmt.Println(numbers) // [1 2 3 4 5 6 7 8 9]
// Strings — lexicographic order (based on byte values)
words := []string{"banana", "apple", "mango", "orange", "durian"}
sort.Strings(words)
fmt.Println(words) // [apple banana durian mango orange]
// Float64
values := []float64{3.14, 1.41, 2.71, 1.73, 2.23}
sort.Float64s(values)
fmt.Println(values) // [1.41 1.73 2.23 2.71 3.14]
// Check whether it's already sorted
fmt.Println(sort.IntsAreSorted(numbers)) // true
fmt.Println(sort.StringsAreSorted(words)) // true
fmt.Println(sort.Float64sAreSorted(values)) // true
// Reverse order — wrap with sort.Reverse
sort.Sort(sort.Reverse(sort.IntSlice(numbers)))
fmt.Println(numbers) // [9 8 7 6 5 4 3 2 1]
}
With the slices Package (Go 1.21+) #
import (
"cmp"
"slices"
)
// slices.Sort — faster and generic
numbers := []int{5, 2, 8, 1, 9, 3}
slices.Sort(numbers)
fmt.Println(numbers) // [1 2 3 5 8 9]
words := []string{"banana", "apple", "mango"}
slices.Sort(words)
fmt.Println(words) // [apple banana mango]
// Reverse order with cmp.Reverse
slices.SortFunc(numbers, func(a, b int) int {
return cmp.Compare(b, a) // b before a = descending
})
fmt.Println(numbers) // [9 8 5 3 2 1]
// Find the minimum and maximum values
min := slices.Min(numbers) // 1
max := slices.Max(numbers) // 9
fmt.Println(min, max)
sort.Slice — Sorting with a Custom Comparator #
sort.Slice is the most used function for sorting structs or complex data — you just define a less(i, j int) bool function that determines whether element i should come before element j.
type Product struct {
ID int
Name string
Price float64
Category string
Stock int
}
products := []Product{
{1, "Laptop", 15000000, "electronics", 10},
{2, "Go Book", 150000, "books", 50},
{3, "Mouse", 250000, "electronics", 30},
{4, "Keyboard", 500000, "electronics", 20},
{5, "Novel", 80000, "books", 100},
}
// Sort by price (ascending)
sort.Slice(products, func(i, j int) bool {
return products[i].Price < products[j].Price
})
for _, p := range products {
fmt.Printf("%-10s Rp%,.0f\n", p.Name, p.Price)
}
// Novel Rp80.000
// Go Book Rp150.000
// Mouse Rp250.000
// Keyboard Rp500.000
// Laptop Rp15.000.000
// Sort by price (descending)
sort.Slice(products, func(i, j int) bool {
return products[i].Price > products[j].Price
})
// Sort by name (alphabetical)
sort.Slice(products, func(i, j int) bool {
return products[i].Name < products[j].Name
})
// Sort by category, then by price within the category
sort.Slice(products, func(i, j int) bool {
if products[i].Category != products[j].Category {
return products[i].Category < products[j].Category
}
return products[i].Price < products[j].Price
})
sort.SliceStable — Preserving Relative Order #
sort.SliceStable guarantees that elements considered equal (the less function returns false for both) stay in the same relative order as before sorting:
flowchart LR
subgraph Input["Input (original order)"]
A["A=1"] --> B["B=1"] --> C["C=2"] --> D["D=1"] --> E["E=2"]
end
subgraph Unstable["sort.Slice — unstable"]
U1["B=1, D=1, A=1, C=2, E=2\nor another valid order"]
end
subgraph Stable["sort.SliceStable — stable"]
S1["A=1, B=1, D=1, C=2, E=2\nrelative order A→B→D preserved"]
end
Input --> Unstable
Input --> Stable
style Unstable fill:#fff3e0
style Stable fill:#e8f5e9type Order struct {
ID int
Customer string
Priority int
Time time.Time
}
orders := []Order{
{1, "Budi", 2, time.Now().Add(-5 * time.Minute)},
{2, "Ani", 1, time.Now().Add(-3 * time.Minute)},
{3, "Candra", 2, time.Now().Add(-4 * time.Minute)},
{4, "Dewi", 1, time.Now().Add(-2 * time.Minute)},
{5, "Eko", 2, time.Now().Add(-1 * time.Minute)},
}
// ANTI-PATTERN: sort.Slice can shuffle orders with the same priority
// Priority-2 orders: {1,3,5} could become {3,1,5} or {5,1,3}, etc.
sort.Slice(orders, func(i, j int) bool {
return orders[i].Priority < orders[j].Priority
})
// CORRECT: SliceStable preserves the time order within the same priority
// Priority-2 orders stay in order: {1,3,5} (by arrival time)
sort.SliceStable(orders, func(i, j int) bool {
return orders[i].Priority < orders[j].Priority
})
// Output: Ani(1), Dewi(1), Budi(2), Candra(2), Eko(2)
// Within the same priority, the time order is preserved
sort.Sort — The Full Interface #
For maximum control or types that are frequently sorted, implement the sort.Interface:
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
// Implementation for a Product slice — can be reused
type ProductSlice []Product
func (p ProductSlice) Len() int { return len(p) }
func (p ProductSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p ProductSlice) Less(i, j int) bool { return p[i].Price < p[j].Price }
// Usage
list := ProductSlice(products)
sort.Sort(list)
sort.Stable(list) // the stable version
sort.IsSorted(list) // check whether it's sorted
// Reverse without defining a new type
sort.Sort(sort.Reverse(list))
Multi-Key Sorting with the Interface #
// Sorting with many criteria using a composable "less" approach
type MultiSorter struct {
items []Product
less []func(a, b Product) bool
}
func (ms *MultiSorter) Sort(items []Product) {
ms.items = items
sort.Sort(ms)
}
func (ms *MultiSorter) Len() int { return len(ms.items) }
func (ms *MultiSorter) Swap(i, j int) { ms.items[i], ms.items[j] = ms.items[j], ms.items[i] }
func (ms *MultiSorter) Less(i, j int) bool {
a, b := ms.items[i], ms.items[j]
var k int
for k = 0; k < len(ms.less)-1; k++ {
less := ms.less[k]
switch {
case less(a, b):
return true
case less(b, a):
return false
}
// equal — try the next criterion
}
return ms.less[k](a, b)
}
// Expressive usage
sorter := &MultiSorter{
less: []func(a, b Product) bool{
func(a, b Product) bool { return a.Category < b.Category }, // category first
func(a, b Product) bool { return a.Price < b.Price }, // then price
func(a, b Product) bool { return a.Name < b.Name }, // then name
},
}
sorter.Sort(products)
sort.Search — Binary Search #
sort.Search performs a binary search on a sorted slice. It finds the smallest index i in [0, n) where f(i) returns true:
flowchart TD
A["sort.Search(n, f)"] --> B["Find the smallest index i\nwhere f(i) = true"]
B --> C{"Data must\nalready be sorted!"}
C --> D["Binary search:\nO(log n)"]
D --> E["Return i\n(n if not found)"]
subgraph Example["Example: find 7 in [1,3,5,7,9,11]"]
F["n=6, find the index i\nwhere arr[i] >= 7"]
G["i=3 → arr[3]=7 ✓"]
end
style D fill:#e8f5e9
style E fill:#e3f2fd// Data must ALREADY BE SORTED before a binary search
numbers := []int{1, 3, 5, 7, 9, 11, 13, 15, 17, 19}
// Find the number 7
target := 7
i := sort.SearchInts(numbers, target)
if i < len(numbers) && numbers[i] == target {
fmt.Printf("Found %d at index %d\n", target, i)
} else {
fmt.Printf("%d not found\n", target)
}
// Found 7 at index 3
// Search for a missing number
i = sort.SearchInts(numbers, 8)
fmt.Printf("8 doesn't exist, its insertion position: %d\n", i)
// 8 doesn't exist, its insertion position: 4
// Generic sort.Search — for any type
words := []string{"apple", "durian", "orange", "mango", "banana"}
search := "orange"
j := sort.SearchStrings(words, search)
if j < len(words) && words[j] == search {
fmt.Printf("Found %q at index %d\n", search, j)
}
// Found "orange" at index 2
// sort.Search for more complex conditions
prices := []float64{50000, 100000, 150000, 200000, 500000, 1000000}
// Find products with price >= 200000 (the first index satisfying it)
threshold := 200000.0
k := sort.Search(len(prices), func(i int) bool {
return prices[i] >= threshold
})
fmt.Printf("Price >= %.0f starts at index %d: %.0f\n",
threshold, k, prices[k])
// Price >= 200000 starts at index 3: 200000
Binary Search with slices (Go 1.21+) #
import "slices"
numbers := []int{1, 3, 5, 7, 9, 11}
// BinarySearch — more ergonomic than sort.Search
i, found := slices.BinarySearch(numbers, 7)
fmt.Println(i, found) // 3 true
i, found = slices.BinarySearch(numbers, 8)
fmt.Println(i, found) // 4 false — the insertion position if you want to add it
// BinarySearchFunc — for structs
products := []Product{
{1, "Book", 80000, "books", 100},
{2, "Mouse", 250000, "electronics", 30},
{3, "Laptop", 15000000, "electronics", 10},
}
// Already sorted by price
slices.SortFunc(products, func(a, b Product) int {
return cmp.Compare(a.Price, b.Price)
})
// Find a product with price 250000
idx, found := slices.BinarySearchFunc(products, 250000.0, func(p Product, price float64) int {
return cmp.Compare(p.Price, price)
})
fmt.Println(idx, found) // 1 true
Sorting with slices.SortFunc (Go 1.21+) #
The slices package introduces generic sorting that’s more expressive and faster than sort.Slice:
import (
"cmp"
"slices"
)
type Employee struct {
Name string
Department string
Salary float64
Joined time.Time
}
employees := []Employee{
{"Budi", "Engineering", 15000000, time.Date(2021, 3, 1, 0, 0, 0, 0, time.Local)},
{"Ani", "Marketing", 12000000, time.Date(2020, 6, 15, 0, 0, 0, 0, time.Local)},
{"Candra", "Engineering", 18000000, time.Date(2019, 1, 10, 0, 0, 0, 0, time.Local)},
{"Dewi", "HR", 10000000, time.Date(2022, 9, 5, 0, 0, 0, 0, time.Local)},
{"Eko", "Engineering", 15000000, time.Date(2020, 3, 1, 0, 0, 0, 0, time.Local)},
}
// Sort by salary descending
slices.SortFunc(employees, func(a, b Employee) int {
return cmp.Compare(b.Salary, a.Salary) // b,a = descending
})
// Multi-criteria sort: department ascending, then salary descending
slices.SortFunc(employees, func(a, b Employee) int {
if n := cmp.Compare(a.Department, b.Department); n != 0 {
return n
}
return cmp.Compare(b.Salary, a.Salary)
})
// Stable sort — preserves the order of equal elements
slices.SortStableFunc(employees, func(a, b Employee) int {
return cmp.Compare(a.Department, b.Department)
})
// Within the same department, the original order is preserved
// Min and Max
newest := slices.MinFunc(employees, func(a, b Employee) int {
return a.Joined.Compare(b.Joined) // Compare for time.Time
})
fmt.Printf("Most recently joined: %s (%s)\n",
newest.Name, newest.Joined.Format("2006-01-02"))
sort.Reverse — Reversing Order #
// Reverse a slice of a primitive type
numbers := []int{3, 1, 4, 1, 5, 9, 2, 6}
sort.Sort(sort.Reverse(sort.IntSlice(numbers)))
fmt.Println(numbers) // [9 6 5 4 3 2 1 1]
// Reverse a string slice
words := []string{"apple", "mango", "orange"}
sort.Sort(sort.Reverse(sort.StringSlice(words)))
fmt.Println(words) // [orange mango apple]
// Reverse a struct slice
sort.Slice(products, func(i, j int) bool {
// Descending: return true if i is GREATER than j
return products[i].Price > products[j].Price
})
// Or with slices (cleaner):
slices.SortFunc(products, func(a, b Product) int {
return cmp.Compare(b.Price, a.Price) // b before a = descending
})
Production Usage Patterns #
Sorting Query Results with Dynamic Criteria #
type SortField string
type Direction string
const (
DirAsc Direction = "asc"
DirDesc Direction = "desc"
)
type SortCriteria struct {
Field SortField
Dir Direction
}
func sortProducts(products []Product, criteria []SortCriteria) {
sort.SliceStable(products, func(i, j int) bool {
a, b := products[i], products[j]
for _, c := range criteria {
var less bool
var equal bool
switch c.Field {
case "name":
less = a.Name < b.Name
equal = a.Name == b.Name
case "price":
less = a.Price < b.Price
equal = a.Price == b.Price
case "stock":
less = a.Stock < b.Stock
equal = a.Stock == b.Stock
case "category":
less = a.Category < b.Category
equal = a.Category == b.Category
default:
continue
}
if equal {
continue // try the next criterion
}
if c.Dir == DirDesc {
return !less
}
return less
}
return false // all criteria are equal
})
}
// Usage: sort by category asc, then price desc
sortProducts(products, []SortCriteria{
{Field: "category", Dir: DirAsc},
{Field: "price", Dir: DirDesc},
})
Top-N Elements Without a Full Sort #
To take the N largest/smallest elements from a large slice, a full sort is wasteful — O(n log n) when we only need O(n):
// Take the 5 most expensive products without a full sort
func topNExpensive(products []Product, n int) []Product {
if n >= len(products) {
// Full sort if n >= slice length
result := make([]Product, len(products))
copy(result, products)
sort.Slice(result, func(i, j int) bool {
return result[i].Price > result[j].Price
})
return result
}
// Partial sort: only sort the first n elements
result := make([]Product, len(products))
copy(result, products)
// Use partial selection sort — O(n*k) is better than O(n log n)
// for small k
for i := 0; i < n; i++ {
maxIdx := i
for j := i + 1; j < len(result); j++ {
if result[j].Price > result[maxIdx].Price {
maxIdx = j
}
}
result[i], result[maxIdx] = result[maxIdx], result[i]
}
return result[:n]
}
Finding a Range with Binary Search #
// Find all products in a price range using binary search
func findPriceRange(products []Product, min, max float64) []Product {
// Make sure it's sorted by price
if !sort.SliceIsSorted(products, func(i, j int) bool {
return products[i].Price < products[j].Price
}) {
sort.Slice(products, func(i, j int) bool {
return products[i].Price < products[j].Price
})
}
// Find the start index (price >= min)
start := sort.Search(len(products), func(i int) bool {
return products[i].Price >= min
})
// Find the end index (price > max)
end := sort.Search(len(products), func(i int) bool {
return products[i].Price > max
})
return products[start:end]
}
// Usage
inRange := findPriceRange(products, 100000, 500000)
fmt.Printf("Products between Rp100.000-Rp500.000: %d items\n", len(inRange))
Removing Duplicates from a Sorted Slice #
// After sorting, duplicates are adjacent — easy to remove
func removeDuplicates(s []string) []string {
if len(s) == 0 {
return s
}
sort.Strings(s)
j := 0
for i := 1; i < len(s); i++ {
if s[i] != s[j] {
j++
s[j] = s[i]
}
}
return s[:j+1]
}
// Generic version with slices (Go 1.21+)
import "slices"
func removeDuplicatesGeneric[T comparable](s []T) []T {
slices.Sort(s)
return slices.Compact(s) // Compact removes consecutive duplicates
}
// Usage
tags := []string{"go", "backend", "go", "api", "backend", "tutorial"}
unique := removeDuplicates(tags)
fmt.Println(unique) // [api backend go tutorial]
Inserting into a Sorted Slice #
// Insert a value at the correct position to keep the order
func insertSorted(s []int, val int) []int {
// Find the right position
i := sort.SearchInts(s, val)
// Insert at position i
s = append(s, 0) // add space
copy(s[i+1:], s[i:]) // shift the elements
s[i] = val // insert
return s
}
// With slices (Go 1.21+)
func insertSortedGeneric[T cmp.Ordered](s []T, val T) []T {
i, _ := slices.BinarySearch(s, val)
return slices.Insert(s, i, val)
}
// Usage
sorted := []int{1, 3, 5, 7, 9}
sorted = insertSorted(sorted, 4)
fmt.Println(sorted) // [1 3 4 5 7 9]
Performance: sort vs slices #
flowchart LR
subgraph Comparison["sort.Slice vs slices.SortFunc"]
P1["sort.Slice\n- Uses interface{}\n (boxing/unboxing)\n- Reflection overhead\n- Available since Go 1\n- More compatible"]
P2["slices.SortFunc\n- Generic, no boxing\n- Faster ~20-30%\n- Go 1.21+ only\n- Cleaner API"]
end
subgraph Choose["Which one?"]
Q{"Go version?"} --> New["Go 1.21+\n→ slices.SortFunc"]
Q --> Old["Go < 1.21\n→ sort.Slice"]
New --> Consistent["Consistently use\nthe slices package"]
end
style P2 fill:#e8f5e9
style P1 fill:#e3f2fd// Comparison benchmark (illustration):
// sort.Slice: ~850 ns/op for 100 elements
// slices.SortFunc: ~650 ns/op for 100 elements
// (~25% faster because generics avoid interface boxing)
// For general applications, this difference isn't significant
// Choose based on the Go version and code consistency
When to Switch to Alternatives #
Keep using sort / slices if:
✓ Sorting slices of all types
✓ Binary search on sorted data
✓ One-off or infrequent sort operations
Consider other data structures if:
✗ Data must always be sorted during insert/delete
→ heap (container/heap) for a priority queue
→ tree-based structure (not in the stdlib, use btree)
✗ Very frequent searches on constantly changing data
→ consider a sorted set or an indexed database
Consider custom algorithms if:
✗ Data is almost sorted → insertion sort O(n) for near-sorted
✗ Only need the N smallest/largest → heap or quickselect O(n)
✗ Sorting integers in a small range → counting sort O(n+k)
✗ Sorting many strings with the same prefix → radix sort
Summary #
sort.Slicefor quick sorting,sort.SliceStableif the relative order of equal elements must be preserved — e.g. sorting by priority when the data is already sorted by time.slices.SortFunc(Go 1.21+) is the faster, generic version ofsort.Slice— use it for new projects with Go 1.21+.- Multi-key sort: compare the first field, if equal compare the second field, etc. — this pattern applies in
sort.Slice,sort.Sort, andslices.SortFuncalike.sort.Searchandslices.BinarySearchonly work on already-sorted data — always make sure the data is sorted before a binary search; the results can’t be trusted otherwise.sort.SearchInts/sort.SearchStringsare shortcuts for binary search on primitive types — the genericslices.BinarySearchis more ergonomic in Go 1.21+.- Stable sorting matters when the data already has a meaningful order (time, ID) and you’re only adding a new sort criterion on top — use
SliceStableorSortStableFunc.- Remove duplicates: sort first, then iterate to remove consecutive equal elements — or use
slices.Compactin Go 1.21+.- For top-N: if N is small compared to the slice length, partial selection sort
O(n*k)is more efficient than a full sortO(n log n).