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:#e3f2fd

Sorting 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:#e8f5e9
type 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 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]
}
// 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.Slice for quick sorting, sort.SliceStable if 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 of sort.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, and slices.SortFunc alike.
  • sort.Search and slices.BinarySearch only 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.SearchStrings are shortcuts for binary search on primitive types — the generic slices.BinarySearch is 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 SliceStable or SortStableFunc.
  • Remove duplicates: sort first, then iterate to remove consecutive equal elements — or use slices.Compact in 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 sort O(n log n).

← Previous: Log Slog   Next: Bytes →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact