An iterator can produce values in an order that already means something: arrival order, file order, database order, or the order established by an earlier stage of a pipeline. If you need to sort those values by one key without scrambling equal-key groups, slices.SortedStableFunc handles both steps at once.

It consumes an iter.Seq, collects the yielded values into a new slice, and sorts that slice with a comparator. When the comparator returns zero, the values keep the same relative order they had in the sequence.

Sort iterator values with slices.SortedStableFunc

Suppose a stream of jobs arrives in this order:

package main

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

type Job struct {
    Queue    string
    Priority int
    ID       string
}

func main() {
    jobs := []Job{
        {Queue: "api", Priority: 2, ID: "A"},
        {Queue: "batch", Priority: 1, ID: "B"},
        {Queue: "api", Priority: 1, ID: "C"},
        {Queue: "batch", Priority: 1, ID: "D"},
        {Queue: "api", Priority: 2, ID: "E"},
    }

    ordered := slices.SortedStableFunc(
        slices.Values(jobs),
        func(a, b Job) int {
            return cmp.Compare(a.Priority, b.Priority)
        },
    )

    fmt.Println(ordered)
}

The result is:

[{batch 1 B} {api 1 C} {batch 1 D} {api 2 A} {api 2 E}]

All priority-1 jobs move before the priority-2 jobs. Within each priority, the original sequence order survives: B stays before C, C before D, and A before E.

The comparator is what defines a tie. Here it compares only Priority, so jobs with the same priority are equal for sorting even when their queue and ID differ.

SortedStableFunc collects instead of modifying the source

slices.SortedStableFunc differs from slices.SortStableFunc in an important way. SortStableFunc receives a slice and rearranges that slice in place. SortedStableFunc receives an iterator and returns a newly collected slice.

In the example above, jobs keeps its original order. Only ordered contains the sorted result. That makes SortedStableFunc a good fit at the boundary between a lazy iterator pipeline and code that needs a materialized, sorted collection.

For an existing slice, slices.Values provides the adapter:

ordered := slices.SortedStableFunc(
    slices.Values(jobs),
    comparePriority,
)

If you already have an iter.Seq[Job], don’t collect it first just to pass it to a sorting function. Feed the sequence directly to SortedStableFunc:

func orderJobs(seq iter.Seq[Job]) []Job {
    return slices.SortedStableFunc(seq, comparePriority)
}

This still materializes the full sequence because sorting needs all values before it can determine the final order. The useful part is that you avoid an unnecessary intermediate slice and keep the pipeline lazy until sorting actually requires storage.

Stable ties preserve sequence order

Stability matters only when the comparator returns zero. Consider this comparator:

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

It deliberately ignores Queue and ID. If two jobs have the same priority, their order in the result comes from the iterator’s yield 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 that comparator, two same-priority jobs with different IDs no longer compare equal. Stable sorting can’t preserve their incoming order because the comparator explicitly gives them an order of its own.

That distinction is useful when choosing the comparison rule. If arrival order should decide ties, leave the tie-breaker out and use the stable sort. If the domain says IDs should decide ties, put that rule in the comparator instead of relying on whatever order happened to arrive upstream.

Use stable sorting when upstream order carries meaning

Iterator pipelines often preserve a useful order before the final sort. A sequence might already be ordered by creation time, source position, or a previous stable operation. Sorting stably by a broader grouping key lets that earlier order survive inside each group.

For example, imagine jobs are yielded oldest first and need to be grouped by priority. A stable priority sort produces priority groups while retaining FIFO order within each priority. You don’t need to add creation time to the comparator merely to reconstruct an order the sequence already has.

There is a trade-off. The final ordering now depends on two things: the comparator and the sequence’s existing order. If callers can produce the sequence in different orders, equal-key results can differ between callers. When deterministic output must be independent of input order, define a complete comparator with explicit tie-breakers.

Stability is most useful when input order is part of the contract, not when it is accidental.

Empty sequences return a nil slice

An empty input doesn’t require a special case. slices.SortedStableFunc returns a nil slice when the sequence yields no values.

For example:

var jobs []Job

ordered := slices.SortedStableFunc(
    slices.Values(jobs),
    comparePriority,
)

fmt.Println(ordered == nil) // true
fmt.Println(len(ordered))   // 0

Usually, code should care about len(ordered) == 0 rather than whether the slice is nil. The distinction can matter at serialization or API boundaries, though, where a nil slice and an allocated empty slice may be represented differently. If a caller requires a non-nil empty slice, normalize the result at that boundary.

The iterator is consumed once

An iter.Seq is a function that yields values to its consumer. SortedStableFunc consumes that sequence while collecting it.

Don’t assume every sequence can be replayed safely. A sequence backed by a channel, scanner, cursor, or other stateful source may represent a one-pass stream. If another part of the program also needs the values, decide deliberately where to materialize them rather than ranging the sequence once and expecting the same data to appear again.

This also means sorting an iterator is not lazy. The function can’t yield the smallest element immediately because a later input value might belong before it. It must first consume the sequence, retain the values, and then sort them.

For large or unbounded streams, that is a real limitation. If the input can grow without a practical bound, collecting all of it may use unacceptable memory or never finish. In that case, the solution usually needs a bounded algorithm, external sorting, or a domain-specific approach rather than SortedStableFunc.

Keep the comparator consistent

The comparison function follows the same convention as other comparator-based helpers in slices: return a negative value when the first element belongs before the second, a positive value when it belongs after, and zero when the two are equal for this ordering.

Using cmp.Compare for ordered fields keeps that contract visible:

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

Avoid implementing integer comparison with subtraction:

return a.Priority - b.Priority

That looks compact but can overflow for values near the integer limits, producing a sign that doesn’t match the intended order. cmp.Compare avoids that failure mode.

The comparator also needs to describe a consistent strict weak ordering. Don’t make it depend on mutable state that changes while sorting, current time, random values, or other inputs that can make the same pair compare differently from one call to the next. Sorting code assumes the ordering rule stays coherent throughout the operation.

If several call sites use the same domain order, name the comparator. Reusing comparePriority is easier to inspect and test than duplicating anonymous comparison functions that can drift apart over time.

Choose between SortedFunc and SortedStableFunc deliberately

slices.SortedFunc also collects an iterator and sorts the resulting slice with a comparator, but it doesn’t promise to preserve the incoming order of elements that compare equal.

Use slices.SortedStableFunc when equal elements have meaningful sequence order. FIFO behavior inside priority groups, source-file order within categories, and an earlier ranking within buckets are typical examples.

Use slices.SortedFunc when equal elements are interchangeable and no caller should depend on their previous order. If you actually need a deterministic tie-breaker such as ID or timestamp, express it in the comparator rather than choosing stable sorting and hoping the input arrives in the desired order.

For code that already owns a slice and is allowed to reorder it, slices.SortStableFunc avoids the collect-from-iterator step and sorts in place. SortedStableFunc is the better match when the source is naturally an iterator or when you want a new sorted slice while leaving an existing source slice untouched.

Materialize at the point where sorting becomes necessary

Iterator pipelines are useful because they can postpone allocation while values are filtered or transformed. Sorting is a natural boundary where that laziness has to end.

Keep the sequence lazy through operations that can work one value at a time, then call slices.SortedStableFunc when you need a complete ordered result and equal-key values must retain their sequence order. If input order isn’t part of the result’s contract, use SortedFunc or add explicit tie-breakers instead. Making that choice visible in the code prevents later callers from depending on an ordering guarantee you never intended to provide.