An iter.Seq is convenient while values are flowing through a pipeline. Sorting changes the problem: a sort needs all of those values available at once. If the next step needs an ordered slice, Go has a standard-library helper that makes that boundary explicit.

Go 1.23 added slices.Sorted. It consumes an iter.Seq of ordered values, collects the values into a new slice, sorts that slice in ascending order, and returns it.

What slices.Sorted actually does

The signature is compact:

func Sorted[E cmp.Ordered](seq iter.Seq[E]) []E

The cmp.Ordered constraint means it works with types that have a natural ordering, such as integers and strings. The input is an iter.Seq[E], not a slice.

Here’s a complete example using slices.Values to expose an existing slice as an iterator:

package main

import (
	"fmt"
	"slices"
)

func main() {
	values := []int{7, 2, 9, 2, 4}

	sorted := slices.Sorted(slices.Values(values))

	fmt.Println(sorted) // [2 2 4 7 9]
	fmt.Println(values) // [7 2 9 2 4]
}

The original values slice is unchanged. slices.Sorted collects iterator output into a new slice and sorts that result, so it doesn’t perform an in-place sort of some hidden source collection.

That distinction is useful when the source really is an iterator. If you already own a slice and are happy to reorder it, slices.Sort(values) is simpler and avoids creating another slice just to sort the same values.

Sorting is a materialization point

Iterators are often useful because they can produce one value at a time. A filter, generator, or traversal doesn’t necessarily need to hold every result in memory.

Sorting can’t preserve that property in the general case. Before slices.Sorted can know that a value belongs at the beginning of the result, it has to consume the sequence and see the other values. The returned slice therefore contains the complete sequence.

Consider a generated sequence:

package main

import (
	"fmt"
	"iter"
	"slices"
)

func countdown(n int) iter.Seq[int] {
	return func(yield func(int) bool) {
		for i := n; i >= 1; i-- {
			if !yield(i) {
				return
			}
		}
	}
}

func main() {
	fmt.Println(slices.Sorted(countdown(5)))
}

The output is:

[1 2 3 4 5]

The generator itself doesn’t need a backing slice, but slices.Sorted creates one because sorting requires materialized data.

This matters for large or unbounded sequences. Calling slices.Sorted on an iterator that never finishes will never return. Calling it on a sequence with millions of values means those values must fit in memory as the result slice. If you only need the smallest few values, a bounded heap or another top-k strategy can be a better fit than collecting and sorting everything.

Sort map keys without depending on map iteration order

A common use for slices.Sorted is turning unordered map keys into deterministic output. Go map iteration order isn’t specified, so directly ranging over a map is the wrong choice when output order matters.

The maps.Keys function returns an iterator over a map’s keys. Feed that iterator to slices.Sorted:

package main

import (
	"fmt"
	"maps"
	"slices"
)

func main() {
	ports := map[string]int{
		"metrics": 9090,
		"http":    8080,
		"admin":   9000,
	}

	for _, name := range slices.Sorted(maps.Keys(ports)) {
		fmt.Printf("%s=%d\n", name, ports[name])
	}
}

The output follows key order:

admin=9000
http=8080
metrics=9090

This pattern is handy for deterministic configuration output, snapshots, generated text, and tests where random map traversal would create noisy differences.

There is still a cost: the keys are collected and sorted. If ordering isn’t observable or required, ranging over the map directly is less work.

An empty sequence returns a nil slice

One edge case is easy to miss. If the sequence is empty, slices.Sorted returns a nil slice rather than an allocated empty slice.

package main

import (
	"fmt"
	"iter"
	"slices"
)

func main() {
	var empty iter.Seq[int] = func(yield func(int) bool) {}

	result := slices.Sorted(empty)

	fmt.Println(len(result))   // 0
	fmt.Println(result == nil) // true
}

For most slice operations, nil and empty slices behave the same: both have length zero and can be ranged over safely. The difference can become visible at boundaries such as serialization or APIs that deliberately distinguish nil from an allocated empty collection.

If a caller requires a non-nil empty slice, normalize the result at that boundary instead of assuming slices.Sorted allocates one.

Use SortedFunc when the natural order isn’t enough

slices.Sorted only accepts cmp.Ordered element types and always uses their natural ascending order. Structs don’t satisfy that constraint, and even ordered values sometimes need a different policy.

For those cases, use slices.SortedFunc:

package main

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

type Job struct {
	Name     string
	Priority int
}

func main() {
	jobs := []Job{
		{Name: "backup", Priority: 2},
		{Name: "alerts", Priority: 1},
		{Name: "cleanup", Priority: 3},
	}

	sorted := slices.SortedFunc(slices.Values(jobs), func(a, b Job) int {
		if n := cmp.Compare(a.Priority, b.Priority); n != 0 {
			return n
		}
		return cmp.Compare(a.Name, b.Name)
	})

	fmt.Println(sorted)
}

The comparison function makes the ordering rule visible: lower priority numbers come first, and names break ties. Explicit tie-breaking is useful when deterministic output matters.

If equal elements must retain their original sequence order, choose slices.SortedStableFunc instead. SortedFunc doesn’t promise stable ordering for elements that compare equal.

Don’t collect twice before sorting

A common detour is to materialize an iterator manually and then sort it:

values := slices.Collect(seq)
slices.Sort(values)

For naturally ordered values, slices.Sorted(seq) expresses the same intent in one operation:

values := slices.Sorted(seq)

The shorter form is useful because it marks the exact point where a lazy sequence becomes an ordered collection. There’s less plumbing for the reader to mentally combine.

That doesn’t mean every Collect followed by a sort should be rewritten. If you need to inspect, validate, or transform the collected slice between those operations, keeping the steps separate can make the code clearer.

Keep ordering at the boundary that needs it

slices.Sorted fits best when values are naturally produced as an iterator but a later operation needs a sorted slice. It is especially convenient with iterator-producing standard-library functions such as maps.Keys and slices.Values.

Treat the call as a real boundary rather than a free iterator transformation: it consumes the entire sequence, allocates a result slice, and sorts that materialized data. When those costs match what the next step needs, slices.Sorted is a direct way to say exactly what the code is doing.