skip to content

Lazy Sequences

Go 1.23 turned a function into a sequence: an iter.Seq yields values to a callback until that callback returns false, and the slices and maps packages gained helpers that produce and consume them.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

17

In Go's `maps` package, what does `maps.Keys(m)` return, and how do you turn it into a slice?

level: juniorimportance: must knowfreq 52%

answer

  1. it is not a []K
  2. nothing has been computed yet
  3. a function value you range over
  4. iter.Seq[K], added in Go 1.23
  5. slices.Collect drains it into a slice

basics

~10 s

maps.Keys returns an iterator of type iter.Seq[K], not a slice. You can range over it directly, or convert it with slices.Collect(maps.Keys(m)) to get a []K. slices.Sorted(maps.Keys(m)) collects and sorts in one call.

solid answer

~40 s

Since Go 1.23, `maps.Keys(m)` returns an `iter.Seq[K]` — a function value you range over, not a `[]K`. That is the single biggest gotcha, because the older `golang.org/x/exp/maps.Keys` returned a slice, so code and muscle memory ported from it will not compile: you cannot index it, call `len` on it, or append to it. To get a real slice you pass it to a collector: `slices.Collect(maps.Keys(m))` builds a `[]K` in unspecified order, and `slices.Sorted(maps.Keys(m))` builds one in ascending order for any `cmp.Ordered` key type. If you only want to loop once, skip the helpers entirely and write `for k := range m` — that allocates nothing. The same shape holds for `maps.Values` (an `iter.Seq[V]`) and `maps.All` (an `iter.Seq2[K, V]`).

code

go · 11 lines
go
m := map[string]int{"a": 1, "b": 2}

// Just looping? No helper needed — and no allocation.
for k := range m {
	_ = k
}

// Need an actual []string to keep, index or return:
keys := slices.Collect(maps.Keys(m)) // order is unspecified
names := slices.Sorted(maps.Keys(m)) // collected, then sorted
_, _ = keys, names

go deeper

for a junior

Recall that maps.Keys hands back an iterator, not a slice, and that slices.Collect is how you get a real []K. Also be ready to say that a plain for k := range m needs no helper at all.

for a middle

Explain that iter.Seq[K] is a function type, so nothing is computed until something ranges it, and say which collector you would use: Collect, Sorted, SortedFunc or AppendSeq, and what each costs.

for a senior

Show judgment about when materialising keys is worth an allocation at all, and spot the review smell of collecting a slice purely to loop over it once in a hot path.

for a principal

Frame the wider tradeoff: returning sequences instead of slices from your own packages pushes the allocation decision to the caller, at the price of an API that is harder to use and pins consumers to Go 1.23 or later.

## What `maps.Keys` actually gives you Go 1.23 added range-over-function and the `iter` package, and with it a set of standard-library helpers that speak in *sequences* rather than slices. `maps.Keys` is one of them: ```go func Keys[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[K] ``` `iter.Seq[K]` is just a named function type: ```go type Seq[V any] func(yield func(V) bool) ``` So the value you get back is a **function**, not data. Nothing has been computed when `maps.Keys(m)` returns. The keys are produced only when something drives that function — either a `for ... range` loop over it, or a helper that consumes it. That is why every slice operation you might reach for fails to compile: ```go keys := maps.Keys(m) n := len(keys) // does not compile: len is not defined on a func type k := keys[0] // does not compile: cannot index a func value ``` ## Why this trips people up Before the standard library had iterators, the community used `golang.org/x/exp/maps`, whose `Keys` returned `[]K` directly. When those helpers were promoted into the standard `maps` package, the signature deliberately changed to an iterator. So the same call expression exists in both worlds with different result types — one of the few places in Go where copying a familiar line into a new file silently changes meaning. If you see `len(maps.Keys(m))` in a code review, the author is thinking of the `x/exp` version. ## Turning a sequence into a slice The `slices` package holds the collectors: - `slices.Collect(seq iter.Seq[E]) []E` — drains the sequence and appends every element into a new slice. `slices.Collect(maps.Keys(m))` is the direct replacement for the old slice-returning helper. The resulting order is whatever the map iteration produced, which is not specified. - `slices.Sorted(seq iter.Seq[E]) []E` — collects and then sorts, for element types satisfying `cmp.Ordered` (integers, floats, strings). `slices.Sorted(maps.Keys(m))` is one call instead of collect-then-`slices.Sort`. - `slices.SortedFunc(seq, cmp)` — the same, with your own comparison function returning a negative, zero or positive `int`, for key types that are not ordered by `<`. - `slices.AppendSeq(dst, seq) []E` — appends into a slice you already have, so you can reuse capacity instead of allocating a fresh backing array each time. Collecting an empty sequence gives you a slice of length zero; do not rely on it being non-nil, and do not rely on `slices.Collect` reserving capacity in advance — it grows the slice as it drains, the same way `append` does. ## The companions `maps.Values(m)` returns `iter.Seq[V]` — the values, again as a sequence. `maps.All(m)` returns `iter.Seq2[K, V]`, a two-value sequence, which is what you want when you need pairs. `iter.Seq2` is why `for k, v := range maps.All(m)` compiles while `for k, v := range maps.Keys(m)` does not: a one-value sequence can only be ranged with one iteration variable. In the other direction, `maps.Collect(seq iter.Seq2[K, V]) map[K]V` builds a map out of a two-value sequence, and `maps.Insert(m, seq)` writes the pairs into an existing map. ## When you need none of this The helpers exist to *adapt* — to hand a map's contents to something that consumes an `iter.Seq`, or to materialise a slice you genuinely need to keep, index, sort or return. If all you are doing is looping once over the keys, `for k := range m` is shorter, allocates nothing and does not depend on Go 1.23. Wrapping it as `for k := range maps.Keys(m)` adds a function call per element and buys nothing; collecting into a slice first adds an allocation on top of that. The rule of thumb: range the map directly to read it, and use `maps.Keys` plus a collector only when the slice itself is the thing you need, or when the consumer's parameter type is `iter.Seq[K]`.

  • Why does the standard library's maps.Keys return an iterator when golang.org/x/exp/maps.Keys returned a slice?
    An iterator lets the caller decide whether to allocate. Ranging it costs nothing extra, feeding it to `slices.Sorted` sorts without a second pass over a temporary, and a consumer that only needs the first match can stop early. The slice version forced an allocation of every key on every caller, even ones that discarded most of them.
  • What is the difference between slices.Collect and slices.Sorted when applied to maps.Keys(m)?
    `slices.Collect` drains the sequence into a new slice and stops there, so the order is whatever came out. `slices.Sorted` does the same collection and then sorts the result in ascending order, and it constrains the element type to `cmp.Ordered`. For a key type that is not ordered by `<` — a struct, say — use `slices.SortedFunc` with your own comparison.
  • How do you get key and value pairs rather than just keys?
    Use `maps.All(m)`, which returns `iter.Seq2[K, V]` and can be ranged as `for k, v := range maps.All(m)`. `maps.Keys` returns a one-value `iter.Seq[K]`, so ranging it with two variables is a compile error. For values alone there is `maps.Values(m)`, an `iter.Seq[V]`.

saying these in an interview costs you the question

  • Says maps.Keys returns a []K you can index
  • Calls len() on the value maps.Keys returns
  • Assumes the keys come back in insertion order
  • Thinks maps.Keys precomputes the keys eagerly
  • Collects into a slice just to loop over it once
open as a page

What does iter.Pull return, and why use it instead of ranging over an iter.Seq?

level: juniorimportance: must knowfreq 40%

basics

~20 s

iter.Pull(seq) returns two functions: next, which gives the next value plus a bool that is false once the sequence is exhausted, and stop, which ends it early. It turns a sequence into a cursor you advance yourself.

open as a page

How do you write a Go function returning iter.Seq[string] that a caller can use in a for-range loop?

level: juniorimportance: must knowfreq 40%

basics

~20 s

Return a closure whose type is func(yield func(string) bool). Inside it, call yield once per element and stop the moment yield returns false. Ranging over the returned function makes the compiler supply yield from the loop body.

open as a page

What is Go's `iter.Seq[V]` type, and what does a value of that type actually hold?

level: juniorimportance: must knowfreq 62%

basics

~20 s

iter.Seq[V] is a function type: func(yield func(V) bool). An iterator is just a function that calls yield once per element, and each yield call becomes one iteration of the for range loop that consumes it.

open as a page

In an iter.Seq iterator you wrote, what must happen when the yield callback returns false, and what if it keeps going?

level: middleimportance: must knowfreq 55%

basics

~20 s

A false result from yield means the consuming loop has stopped. The iterator must produce no further values and return promptly, running its deferred cleanup. Calling yield again after it returned false is a run-time panic.

open as a page

What do Go's `slices.Values` and `slices.All` return, and when would you need them?

level: middleimportance: should knowfreq 38%

basics

~20 s

slices.Values(s) returns an iter.Seq of the elements; slices.All(s) returns an iter.Seq2 of index and element pairs. Both adapt a slice into a sequence value so it can be passed to code whose parameter type is an iterator.

open as a page

How do you merge two sorted iter.Seq[string] sequences in order using iter.Pull?

level: middleimportance: should knowfreq 26%

basics

~20 s

Pull both sequences to get two independent cursors, prime each with one call to next, then repeatedly emit the smaller head and advance only that cursor. When one side's bool goes false, keep draining the other. Defer both stop functions.

open as a page

What does the stop function from iter.Pull do to the suspended sequence, and why defer it?

level: middleimportance: should knowfreq 32%

basics

~20 s

Calling stop resumes the suspended sequence with its pending yield returning false, so the iterator returns and its deferred cleanup runs. Deferring stop right after iter.Pull covers every exit path, including early returns and panics.

open as a page

In an iter.Seq2[string, error] directory walker, how do you report a read failure to the ranging caller?

level: middleimportance: should knowfreq 38%

basics

~20 s

Call yield with a zero value for the first half and the non-nil error for the second, then either return or continue past the failed entry. The caller checks the error before using the value.

open as a page

In a `for range` over an `iter.Seq`, what does `break` in the loop body do to the iterator function?

level: middleimportance: should knowfreq 55%

basics

~20 s

The yield call that delivered the current element returns false instead of true. That is the iterator's signal to stop: it should run its cleanup and return without calling yield again. Calling yield after it has returned false panics.

open as a page

What does `iter.Seq2[K, V]` yield, and when would you return one instead of `iter.Seq`?

level: middleimportance: should knowfreq 45%

basics

~20 s

iter.Seq2[K, V] is func(yield func(K, V) bool): each yield call delivers two related values at once. Return one when every element has a companion the caller would otherwise have to reconstruct — an index, a key, a page number.

open as a page

`slices.Collect` over an unbounded sequence OOMs a service — how do you confirm that and fix it?

level: seniorimportance: should knowfreq 34%

basics

~20 s

slices.Collect drains the whole sequence into one slice, so memory grows with the number of elements yielded and the iterator's laziness is discarded. Confirm with a heap profile: inuse_space dominated by the growing backing array under that call. Fix by streaming or capping instead of collecting.

open as a page

Your iter.Seq2 file iterator opens a file it reads from; how do you guarantee it closes when the caller breaks early?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Open the file inside the returned closure and defer its Close there, never in the factory. That closure's frame lasts exactly as long as the loop and unwinds on an early break, so Close runs once.

open as a page

In Go's `maps` package, when do you use `maps.Insert` instead of `maps.Collect`?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

maps.Collect allocates and returns a brand new map from a two-value sequence. maps.Insert writes the pairs into a map you already have and returns nothing. Use Insert to merge several sequences into one map, or to fill a map you pre-sized.

open as a page

Why would an iter.Pull merge step leave 40,000 goroutines parked in the goroutine profile?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

Each iter.Pull runs its sequence on a coroutine parked inside yield between values. Abandoning the cursor without calling stop leaves that sequence parked forever, its cleanup unrun, one goroutine per call. The fix is defer stop() after every Pull.

open as a page

An SDK's `iter.Seq[Item]` yields 200 items on the first range and nothing on a second. Why?

level: seniorimportance: nice to knowfreq 32%

basics

~20 s

Nothing in iter.Seq's contract makes a sequence restartable. This one closes over a drained response body or a spent page cursor, so the second range calls it again, it yields nothing and returns normally: an empty result, not an error.

open as a page

When should a widely imported Go package export an iter.Seq instead of returning a slice or taking a callback?

level: principalimportance: nice to knowfreq 26%

basics

~10 s

Export a sequence when results are large or streamed, callers stop early, or the loop must hold a resource open. Return a slice when the result is small and already in memory.

open as a page