A sort can produce the correct key order and still damage information you meant to keep. Suppose records already arrive in creation order, and the UI groups them by status. If records with the same status should remain in creation order, an unstable sort doesn’t provide the contract you need.
slices.SortStableFunc handles that case. It sorts with a custom comparator and preserves the original relative order of elements that the comparator treats as equal.
Sort records without disturbing equal groups
Consider jobs that arrive in queue order. The display should group them by priority, but jobs at the same priority must stay in their incoming sequence:
package main
import (
"cmp"
"fmt"
"slices"
)
type Job struct {
ID string
Priority int
}
func main() {
jobs := []Job{
{ID: "A", Priority: 2},
{ID: "B", Priority: 1},
{ID: "C", Priority: 2},
{ID: "D", Priority: 1},
}
slices.SortStableFunc(jobs, func(a, b Job) int {
return cmp.Compare(a.Priority, b.Priority)
})
fmt.Println(jobs)
}The result is:
[{B 1} {D 1} {A 2} {C 2}]B remains before D, and A remains before C. Those pairs compare equal on priority, so the stable sort retains their relative positions.
The comparator follows the same contract as slices.SortFunc: return a negative value when a belongs before b, a positive value when it belongs after b, and zero when the two values are equal for sorting purposes. It must define a strict weak ordering.
Stability depends on comparator equality
The key phrase is compare equal. slices.SortStableFunc doesn’t inspect struct fields to decide which records are equivalent. It only sees the integer returned by your comparator.
This comparator groups jobs by priority:
func(a, b Job) int {
return cmp.Compare(a.Priority, b.Priority)
}Two jobs with priority 2 compare as zero even when their IDs differ. Their incoming order is therefore preserved.
Now add ID as a tie-breaker:
func(a, b Job) int {
if n := cmp.Compare(a.Priority, b.Priority); n != 0 {
return n
}
return cmp.Compare(a.ID, b.ID)
}Jobs with the same priority but different IDs no longer compare equal. Stability has nothing to preserve between them because the comparator gives them a defined order.
That distinction matters when reviewing a sort. A stable algorithm can’t compensate for a comparator that resolves every tie.
Use stable sorting to preserve an existing sequence
Stable sorting is useful when the current sequence already carries meaning. That sequence might come from a database query, an earlier sort, a queue, or a deliberate ordering step in the program.
Suppose events are already ordered by timestamp and need to be grouped by severity:
type Event struct {
Message string
Severity int
UnixTime int64
}
slices.SortStableFunc(events, func(a, b Event) int {
return cmp.Compare(a.Severity, b.Severity)
})If the input is chronological, events inside each severity group remain chronological after the sort. The comparator doesn’t need to mention UnixTime because the existing order supplies the secondary sequence.
This can make intent clearer than repeating the timestamp comparison as a tie-breaker. It also differs in one practical respect: a timestamp tie-breaker can’t distinguish two records with the same timestamp, while stability retains whatever order those records already had.
The trade-off is that the result now depends on input order. If callers can supply records in arbitrary sequences, preserving that sequence may not give deterministic output across calls. In that case, explicit tie-breakers are often a better contract.
Build multi-key ordering with repeated stable sorts
Stable sorting also supports a useful multi-pass pattern. Sort the least significant key first, then sort the most significant key.
For example, records should be ordered by department first and score second:
type Result struct {
Name string
Department string
Score int
}
slices.SortStableFunc(results, func(a, b Result) int {
return cmp.Compare(b.Score, a.Score)
})
slices.SortStableFunc(results, func(a, b Result) int {
return cmp.Compare(a.Department, b.Department)
})The first pass puts higher scores first. The second pass groups departments, and stability keeps the score order inside each department.
Order of operations is critical. If department is the primary key, it must be sorted last in this pattern. Reversing the calls makes score the primary key instead.
A single comparator is often easier to read when all keys are known in one place:
slices.SortStableFunc(results, func(a, b Result) int {
if n := cmp.Compare(a.Department, b.Department); n != 0 {
return n
}
return cmp.Compare(b.Score, a.Score)
})Use repeated stable sorts when ordering is assembled in separate stages or when preserving an earlier ordering step is itself part of the design. For a fixed two-field order, one comparator is usually more direct.
SortStableFunc changes the supplied slice
Like the other in-place sorting functions in slices, slices.SortStableFunc rearranges the elements of the slice passed to it. It doesn’t return a sorted copy.
A second slice value can still refer to the same backing array:
jobs := []Job{
{ID: "A", Priority: 2},
{ID: "B", Priority: 1},
}
alias := jobs
slices.SortStableFunc(alias, func(a, b Job) int {
return cmp.Compare(a.Priority, b.Priority)
})
fmt.Println(jobs)The order visible through jobs changes too.
If the original sequence must remain available, clone before sorting:
sorted := slices.Clone(jobs)
slices.SortStableFunc(sorted, func(a, b Job) int {
return cmp.Compare(a.Priority, b.Priority)
})slices.Clone copies the outer slice storage. For elements containing pointers, maps, slices, or similar references, it remains a shallow copy; referenced data is still shared.
Choose stable sorting for a real ordering requirement
It can be tempting to use a stable sort everywhere because retaining equal-item order sounds safer. That isn’t automatically a better choice.
Use slices.SortStableFunc when the order among equal elements has meaning. Examples include retaining arrival sequence inside groups, preserving a previous sort key, or keeping source order in a presentation layer.
Use slices.SortFunc when equal elements have no required relative order. Its contract explicitly doesn’t guarantee stability, so code using it shouldn’t depend on the observed order of ties.
If output must be independent of input sequence, define enough tie-breakers to establish the order you need. Stability preserves existing information; it doesn’t create a deterministic secondary key.
Keep the comparator consistent
Stable sorting still relies on a valid comparator. Don’t base comparison results on mutable counters, random values, or state that can change while sorting.
This comparator is invalid because repeated comparisons can disagree:
flip := false
slices.SortStableFunc(jobs, func(a, b Job) int {
flip = !flip
if flip {
return cmp.Compare(a.Priority, b.Priority)
}
return cmp.Compare(b.Priority, a.Priority)
})The sorting function expects a strict weak ordering. If the comparator contradicts itself, stability doesn’t repair the ordering relation.
Domain-specific special values also need a clear rule. If a field can represent an unknown state, decide where that state belongs and return comparison values consistently. Comparator tests should include equal keys, reversed input, already ordered input, and any special states accepted by the data model.
Preserve order only when it carries meaning
slices.SortStableFunc is a precise tool for one requirement: custom sorting where equal elements must retain their incoming relative order. Keep the comparator focused on the key that defines each group, and let stability preserve the sequence inside those groups.
Before choosing it, identify what the incoming order represents. If that order is meaningful, stable sorting keeps it intact. If the result needs a fixed secondary order instead, encode that order as a tie-breaker. That decision makes the sort’s contract visible rather than leaving tie behavior to accident.