Sorting a slice of structs usually starts with a simple requirement such as “priority first, then ID.” The awkward part is expressing that ordering clearly enough that sorting, validation, and binary search can all agree on it.

slices.SortFunc handles this directly. You give it the slice and a comparator that defines the order. It sorts the existing slice in place, so there is no separate result to assign.

Sort a struct slice with slices.SortFunc

Suppose a queue contains jobs that should be ordered by ascending priority and then by ID when priorities match:

package main

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

type Job struct {
    Priority int
    ID       string
}

func compareJob(a, b Job) int {
    if n := cmp.Compare(a.Priority, b.Priority); n != 0 {
        return n
    }
    return cmp.Compare(a.ID, b.ID)
}

func main() {
    jobs := []Job{
        {Priority: 2, ID: "cleanup"},
        {Priority: 1, ID: "backup"},
        {Priority: 1, ID: "archive"},
    }

    slices.SortFunc(jobs, compareJob)
    fmt.Println(jobs)
}

The output is:

[{1 archive} {1 backup} {2 cleanup}]

The comparator returns a negative value when a belongs before b, zero when they have the same position in the ordering, and a positive value when a belongs after b. cmp.Compare is a convenient way to produce those results for ordered fields such as integers and strings.

Unlike slices.Sort, which works with ordered element types, slices.SortFunc accepts any element type. That is what makes it useful for structs: the language does not guess which struct fields define the order.

Build multi-field ordering one comparison at a time

A multi-field comparator is easier to audit when each field gets a clear decision point. Compare the primary key first and return immediately when it differs. Only inspect the next field when the first comparison is equal.

func compareJob(a, b Job) int {
    if n := cmp.Compare(a.Priority, b.Priority); n != 0 {
        return n
    }
    return cmp.Compare(a.ID, b.ID)
}

This pattern also makes the tie-breaking rule explicit. Two jobs with the same priority are not equal for sorting purposes if their IDs differ.

That distinction matters when the same ordering is reused elsewhere. For example, if a binary search assumes jobs are ordered by priority and ID, sorting by priority alone does not establish the required precondition. Keeping one named comparator and reusing it avoids two subtly different definitions of “sorted.”

For three or four fields, continue the same pattern rather than compressing the logic into a complicated expression. Comparator code is small, but a wrong sign or missing tie-breaker can produce results that look almost correct and are harder to notice than a compile error.

Reverse a field for descending order

Descending order does not require sorting and then reversing the whole slice. Define the desired direction in the comparator instead.

To put higher priorities first while keeping IDs ascending inside each priority:

func compareJobPriorityDescending(a, b Job) int {
    if n := cmp.Compare(b.Priority, a.Priority); n != 0 {
        return n
    }
    return cmp.Compare(a.ID, b.ID)
}

Notice that only the arguments for the priority comparison are reversed. Reversing the final slice would reverse the ID order too, producing descending IDs inside each priority group.

This is a useful reason to think of the comparator as the actual ordering rule, not merely an implementation detail passed to the sorting function.

SortFunc changes the original slice

slices.SortFunc sorts in place. Any code that can see the same slice contents will observe the new element order after the call.

jobs := []Job{
    {Priority: 2, ID: "cleanup"},
    {Priority: 1, ID: "archive"},
}

slices.SortFunc(jobs, compareJob)
// jobs is now ordered.

If the caller’s order must remain untouched, clone the slice first:

sortedJobs := slices.Clone(jobs)
slices.SortFunc(sortedJobs, compareJob)

The clone gives you a separate backing array for the elements in the slice. For structs containing pointers, maps, slices, or other reference-like fields, this is still a shallow copy: sorting the clone won’t reorder the original slice, but both copies can still refer to the same nested data.

Nil and empty slices need no special guard. Sorting either is valid, and a nil slice remains nil.

Equal elements do not keep a guaranteed relative order

SortFunc is not guaranteed to be stable. If the comparator returns zero for two elements, you should not rely on those elements retaining their original relative order.

Consider sorting only by priority:

byPriority := func(a, b Job) int {
    return cmp.Compare(a.Priority, b.Priority)
}

slices.SortFunc(jobs, byPriority)

Jobs with the same priority compare equal even if their IDs differ. Their relative order after SortFunc is unspecified by the stability contract.

If preserving the incoming order of equal elements is part of the requirement, use slices.SortStableFunc with the same comparator instead. Another option is to add a real tie-breaker, such as ID, when that field genuinely belongs in the ordering. Those choices are not interchangeable: stable sorting preserves prior order, while a tie-breaker defines a new order.

Avoid subtraction in integer comparators

A comparator sometimes gets written like this:

return a.Priority - b.Priority

It looks compact, but subtraction can overflow for integer values near the type’s limits. If overflow changes the sign, the comparator can claim that a larger value belongs before a smaller one.

Use cmp.Compare instead:

return cmp.Compare(a.Priority, b.Priority)

The comparator also needs to define a consistent strict weak ordering. In practical terms, don’t make comparisons contradict each other or depend on mutable state that can change while the sort is running. A comparator that reads a changing external ranking table, for example, can stop describing one coherent order partway through the operation.

Reuse the comparator as a domain rule

Once a comparator expresses the order correctly, give it a name and reuse it. slices.IsSortedFunc can validate the same order, and slices.BinarySearchFunc can search data arranged according to a compatible comparison rule.

That reuse is more than code deduplication. It keeps the application’s assumptions aligned. If one call site sorts by priority and ID while another checks only priority, both pieces of code can be locally reasonable and still disagree about the collection’s invariant.

Keep the comparator close to the type or operation whose ordering it defines. When the rule changes, there is then one obvious place to update it and a smaller chance that sorting and searching drift apart.

Choose stability deliberately

For most struct sorting, start by writing down the actual field order and encoding it in a named comparator. Use slices.SortFunc when equal elements do not need to retain their previous order. Switch to slices.SortStableFunc when that previous order carries meaning rather than adding an artificial tie-breaker just to make the output deterministic.

The key is to make the comparator match the domain rule exactly. Once that is true, sorting a struct slice becomes a straightforward operation instead of a collection of one-off less functions scattered through the codebase.