Code that depends on binary search, ordered output, or merge-style processing often assumes a slice is already sorted. For built-in ordered values, slices.IsSorted can check that assumption. Structs and domain-specific orderings need a comparator, which is where slices.IsSortedFunc fits.

The function doesn’t rearrange anything. It answers a narrower question: does this slice already follow the order described by this comparator?

Check a struct slice by one field

slices.IsSortedFunc accepts a slice and a comparison function:

func IsSortedFunc[S ~[]E, E any](x S, cmp func(a, b E) int) bool

The comparator returns a negative value when a belongs before b, a positive value when it belongs after b, and zero when the two values are equal according to the chosen ordering.

For records ordered by numeric ID:

package main

import (
    "cmp"
    "fmt"
    "slices"
)

type User struct {
    ID   int
    Name string
}

func main() {
    users := []User{
        {ID: 10, Name: "Mira"},
        {ID: 20, Name: "Raka"},
        {ID: 40, Name: "Sari"},
    }

    sorted := slices.IsSortedFunc(users, func(a, b User) int {
        return cmp.Compare(a.ID, b.ID)
    })

    fmt.Println(sorted)
}

The output is:

true

Changing the IDs to 10, 40, 20 makes the result false. The names don’t affect the check because the comparator doesn’t inspect them.

Match the check to the operation that depends on it

An order check is most useful when it validates a real precondition. slices.BinarySearchFunc, for example, requires its input to be sorted according to the comparator used for searching.

Keeping those rules aligned prevents a subtle class of bugs:

func compareUserID(a, b User) int {
    return cmp.Compare(a.ID, b.ID)
}

if !slices.IsSortedFunc(users, compareUserID) {
    return errors.New("users must be sorted by ID")
}

If later code searches by ID, this check documents and enforces the same data contract.

Avoid validating with one key and consuming with another. A slice can be perfectly sorted by Name while being unordered by ID. Both statements can be true at the same time because sortedness only has meaning relative to an ordering rule.

Equal keys are still sorted

Adjacent values may compare equal without breaking sorted order.

users := []User{
    {ID: 10, Name: "Mira"},
    {ID: 20, Name: "Raka"},
    {ID: 20, Name: "Sari"},
    {ID: 40, Name: "Tono"},
}

sorted := slices.IsSortedFunc(users, func(a, b User) int {
    return cmp.Compare(a.ID, b.ID)
})

fmt.Println(sorted) // true

This matters when the comparator represents a non-unique key. If the application requires unique IDs, IsSortedFunc alone can’t enforce that stronger rule. A separate uniqueness check is needed.

You can also make the comparator include a tie-breaker when the desired order requires one:

func compareUser(a, b User) int {
    if n := cmp.Compare(a.ID, b.ID); n != 0 {
        return n
    }
    return cmp.Compare(a.Name, b.Name)
}

Now two records with the same ID must also appear in ascending name order to satisfy the check.

Validate multi-field ordering explicitly

Composite ordering is common in logs, versions, and grouped records. The comparator should follow the same field priority used when sorting.

type Version struct {
    Major int
    Minor int
}

versions := []Version{
    {Major: 1, Minor: 0},
    {Major: 1, Minor: 4},
    {Major: 2, Minor: 0},
}

sorted := slices.IsSortedFunc(versions, func(a, b Version) int {
    if n := cmp.Compare(a.Major, b.Major); n != 0 {
        return n
    }
    return cmp.Compare(a.Minor, b.Minor)
})

fmt.Println(sorted) // true

A sequence such as 1.4, 1.0, 2.0 fails because the minor values move backward inside the Major == 1 group.

One easy mistake is to sort with (Major, Minor) and validate with (Minor, Major). Each comparator can be internally consistent, but they describe different layouts. Reusing a named comparator for sorting and validation removes that mismatch:

slices.SortFunc(versions, compareVersion)
fmt.Println(slices.IsSortedFunc(versions, compareVersion))

Descending order is valid too

The word “sorted” doesn’t have to mean ascending numeric order. The comparator defines the direction.

For scores stored from highest to lowest:

scores := []int{100, 90, 75, 60}

sorted := slices.IsSortedFunc(scores, func(a, b int) int {
    return cmp.Compare(b, a)
})

fmt.Println(sorted) // true

Reversing the arguments to cmp.Compare defines descending order. Using the ordinary cmp.Compare(a, b) comparator on that same slice would return false.

This makes IsSortedFunc useful for priority lists and rankings where descending order is intentional rather than an error.

Empty and single-element slices pass

An empty slice has no adjacent pair that violates the comparator, so it is sorted. The same applies to a slice containing one element.

var empty []User
one := []User{{ID: 10, Name: "Mira"}}

byID := func(a, b User) int {
    return cmp.Compare(a.ID, b.ID)
}

fmt.Println(slices.IsSortedFunc(empty, byID)) // true
fmt.Println(slices.IsSortedFunc(one, byID))   // true

This behavior is convenient at API boundaries because callers don’t need special branches for small inputs before validating order.

Comparator quality matters

IsSortedFunc can only judge the sequence using the comparator it receives. A comparator with inconsistent rules can make the result meaningless.

Subtraction is also a poor general-purpose integer comparator:

func(a, b int) int {
    return a - b
}

For fixed-width integers, subtraction can overflow. cmp.Compare(a, b) expresses the intent directly without relying on the arithmetic difference.

For strings, decide whether bytewise ordering, case folding, or another domain rule is intended, then use that same rule everywhere the slice is sorted, checked, or searched.

Check at boundaries instead of everywhere

Repeatedly checking a large slice before every operation adds work without improving correctness if the program itself owns and preserves the ordering invariant.

A better place for validation is often a boundary where ordered data enters the system: decoded input, a function receiving caller-owned data, or a test that verifies a producer’s contract.

Inside code that owns the slice, preserve order when mutating it. Insert at the correct position, re-sort after bulk changes, or keep mutation behind a small API. Then IsSortedFunc can remain a targeted assertion rather than a routine tax on every read.

Treat sortedness as a named contract

slices.IsSortedFunc is most useful when the ordering has a clear name: by ID, by timestamp descending, by version tuple, or by another domain key.

Put that rule in a comparator that sorting, validation, and search code can share where their signatures allow it. Then use the check at the boundaries that can actually violate the contract. The result is simple, but it gives ordered-slice code a concrete guard against data arriving in the wrong sequence.