Sometimes sorting by one field is only half the requirement. You may want jobs grouped by priority while keeping their arrival order inside each priority, or records grouped by category without disturbing an earlier ranking.

slices.SortStableFunc is built for that case. It sorts a slice in place using a custom comparator, but elements that compare equal keep their original relative order.

Preserve equal elements with slices.SortStableFunc

Suppose jobs arrive in this order:

package main

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

type Job struct {
    Priority int
    ID       string
}

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

    slices.SortStableFunc(jobs, func(a, b Job) int {
        return cmp.Compare(a.Priority, b.Priority)
    })

    fmt.Println(jobs)
}

The result is:

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

The priority-1 jobs move ahead of the priority-2 jobs. Within each priority, though, the incoming order is preserved: backup stays before archive, and cleanup stays before report.

That behavior comes from the comparator returning zero for jobs with the same priority. Stable sorting treats those jobs as equal for ordering purposes and preserves their relative positions.

Stability depends on what the comparator calls equal

A stable sort doesn’t decide which fields matter. The comparator does.

This comparator compares priority only:

func comparePriority(a, b Job) int {
    return cmp.Compare(a.Priority, b.Priority)
}

Two jobs with the same priority therefore compare equal even when their IDs differ. slices.SortStableFunc preserves their existing order.

Now add ID as a tie-breaker:

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

With this comparator, same-priority jobs are equal only when their IDs also compare equal. Stability no longer preserves arrival order between backup and archive, because the comparator explicitly says archive belongs first.

This is a common source of confusion. Stable sorting preserves order only among elements for which the comparator returns zero. A tie-breaker removes that equality.

Stable sorting is useful for layered ordering

One practical use for stable sorting is applying ordering rules in stages. If a slice is already arranged by a secondary key, a stable sort by the primary key preserves the secondary ordering inside each primary-key group.

For example, start with jobs ordered by ID:

archive   priority 1
backup    priority 1
cleanup   priority 2
report    priority 2

A stable sort by priority keeps the ID ordering within each priority group. This can be useful when the earlier order comes from another meaningful operation, such as an explicit rank, arrival sequence, or previous stable sort.

There is a caveat: layered sorting can make the final ordering harder to understand because part of the rule lives in the slice’s existing state. When the complete order is naturally expressed as priority, then ID, a single comparator that compares both fields is usually clearer.

Use stability when the previous order itself carries meaning, not merely as a substitute for defining a missing tie-breaker.

SortStableFunc changes the original slice

Like slices.SortFunc, slices.SortStableFunc sorts in place. Code holding the same slice will observe the reordered elements.

If you need a sorted result without changing the caller’s order, clone first:

ordered := slices.Clone(jobs)
slices.SortStableFunc(ordered, comparePriority)

The cloned slice has a separate backing array for its elements, so reordering ordered doesn’t reorder jobs. This is still a shallow copy. If a struct contains a map, slice, pointer, or another reference-like value, both copies can still refer to the same nested data.

Nil and empty slices don’t require a guard. Calling slices.SortStableFunc on either is valid; a nil slice remains nil.

Choose SortFunc when stability has no meaning

Stable sorting isn’t automatically the better default. If equal elements are genuinely interchangeable, slices.SortFunc expresses the requirement without promising preservation of their previous order.

Use slices.SortStableFunc when relative order among equal elements is observable and meaningful. Examples include preserving arrival order within a priority, retaining an earlier ranking inside groups, or keeping records with the same key in source order.

Use slices.SortFunc when the comparator defines everything you care about and no code should depend on the prior order of equal elements.

The distinction also affects maintenance. A stable sort can quietly encode a dependency on earlier ordering. If that earlier ordering changes upstream, the output can change even though the comparator has not. A full multi-field comparator is often easier to reason about when every ordering rule belongs in one place.

Keep the comparator consistent

slices.SortStableFunc uses the comparator in the same way as slices.SortFunc: a negative result means the first element belongs before the second, a positive result means it belongs after, and zero means the elements are equal for this ordering.

The comparator needs to describe a consistent strict weak ordering. Avoid comparisons that depend on state changing during the sort, and avoid integer subtraction such as this:

return a.Priority - b.Priority

Subtraction can overflow and produce the wrong sign near integer limits. cmp.Compare states the intent directly without that problem:

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

If the comparator is reused by validation or search code, give it a name rather than repeating anonymous functions. One shared ordering rule is easier to test and less likely to drift between call sites.

Use stability when existing order is part of the data

Before choosing a stable sort, identify what a zero comparator result means in the application. If two elements compare equal and their current order should survive, slices.SortStableFunc makes that requirement explicit.

If their current order doesn’t matter, use the simpler non-stable sort. If another field should decide the tie, put that field in the comparator instead. Those three cases produce different contracts, and choosing deliberately prevents code from depending on an ordering accident.