An iter.Seq can produce values without storing them all at once. That representation is useful while values are flowing through iterator-based code, but many APIs still need an ordinary slice. Since Go 1.23, slices.Collect provides that materialization boundary directly.
The function consumes a one-value sequence and returns a newly built slice containing each yielded value in sequence order. It is small API surface, but its allocation, ownership, and empty-input behavior are useful to make explicit.
Collection turns iteration into storage
The signature is:
func Collect[E any](seq iter.Seq[E]) []EA sequence controls production through its yield callback. Collect drives that sequence to completion and appends every yielded value to the result.
package main
import (
"fmt"
"iter"
"slices"
)
func evenBelow(limit int) iter.Seq[int] {
return func(yield func(int) bool) {
for n := 0; n < limit; n += 2 {
if !yield(n) {
return
}
}
}
}
func main() {
values := slices.Collect(evenBelow(8))
fmt.Println(values)
}The resulting slice contains [0 2 4 6]. The iterator remains responsible for producing values; Collect supplies the concrete storage that retains them after iteration finishes.
This boundary is distinct from ranging over the sequence. A range loop can process one value and discard it before the next arrives. Collection retains all yielded elements, so the result necessarily requires storage proportional to the number of values produced.
Yield order becomes slice order
Collect does not sort, deduplicate, or otherwise reinterpret a sequence. The order in which the sequence calls yield is the order stored in the returned slice.
seq := func(yield func(string) bool) {
for _, value := range []string{"ready", "running", "done"} {
if !yield(value) {
return
}
}
}
states := slices.Collect(seq)
// [ready running done]That property makes collection a straightforward adapter between an iterator-producing component and an API that accepts []E. Ordering policy stays with the producer rather than being hidden inside the conversion.
For an empty sequence, Collect returns a nil slice. Code that distinguishes nil from a non-nil empty slice should account for that documented result.
empty := func(yield func(int) bool) {}
values := slices.Collect(empty)
fmt.Println(values == nil)
// trueMost slice operations treat nil and empty slices similarly, but serialization formats and explicit nil checks can preserve the distinction.
The result has its own slice storage
The implementation of Collect starts from a nil slice and appends sequence values into it. As a result, the returned slice header and backing storage belong to the collection result rather than aliasing a source slice merely because the sequence was created from one.
source := []int{10, 20, 30}
copyOfValues := slices.Collect(slices.Values(source))
copyOfValues[0] = 99
fmt.Println(source)
// [10 20 30]This is still a shallow element copy. If an element itself contains a pointer, map, slice, or another reference-bearing value, collecting it does not recursively duplicate the referenced data.
source := [][]int{{1, 2}, {3, 4}}
collected := slices.Collect(slices.Values(source))
collected[0][0] = 9
fmt.Println(source[0])
// [9 2]The outer slice storage is separate, while the inner slice value still refers to the same backing array. That follows normal Go assignment semantics for the element type.
Collection consumes the complete sequence
A range loop can stop with break as soon as it has enough information. Collect has no limit parameter and consumes values until the sequence finishes.
That makes it a poor fit for an unbounded sequence. A producer that never terminates gives Collect no natural completion point, and retained storage continues to grow as values arrive.
For bounded sequences, the same eager behavior can be exactly the needed boundary. A function may expose an iterator to avoid committing callers to storage, while a caller that requires random access or repeated traversal can materialize the sequence at the edge where those properties become necessary.
The distinction also affects error design. iter.Seq[E] carries only values, so an operation that needs to communicate an error must represent that state through its element type, surrounding API, or a different control structure. slices.Collect does not add an error channel to the sequence contract.
Collect and AppendSeq express different ownership intent
Go 1.23 also added slices.AppendSeq. It appends sequence values to a slice supplied by the caller:
base := []int{1, 2}
more := slices.Values([]int{3, 4})
values := slices.AppendSeq(base, more)
// [1 2 3 4]Collect can be viewed as the case where no existing destination is supplied. In the standard library implementation, it delegates to AppendSeq with a nil destination.
The API choice can therefore state something about ownership. Collect asks for a fresh result containing only sequence values. AppendSeq extends an existing logical collection and may reuse that slice’s available capacity.
Neither function makes the sequence lazy after the call returns. Both consume it during the call; the difference is the destination into which yielded values are appended.
Materialization belongs at a deliberate boundary
Iterator code can postpone allocation when each value can be handled as it arrives. Slice-oriented code gains indexing, a stable length, repeated traversal, and compatibility with APIs that operate on []E. slices.Collect is the explicit transition between those representations.
Keeping that transition visible helps preserve the useful property of an iterator: values do not need to become retained storage until some part of the program actually requires a slice.