# Go Iterators Rangefunc

> Guides writing and consuming Go push iterators (range-over-func, Go 1.23) — an iter.Seq[V] is func(yield func(V) bool), the iterator calls yield per element and MUST stop calling it the moment yield returns false (the central bug is ignoring that bool — the runtime panics on a yield call after it returned false), range translates break/continue/return through yield, iter.Pull converts push→pull and you MUST call its stop, and the restraint rule is don't wrap a slice you already have in an iterator — return the slice. Covers iter.Seq/Seq2, slices.All/Values/Backward/Collect/Sorted, maps.Keys/Values/Collect, and Seq2 with a trailing error. Auto-invokes when writing or editing iterator functions, iter.Seq/Seq2, range-over-func, yield callbacks, iter.Pull, or on "how do I make this rangeable" / "should this return an iterator or a slice". Honor yield's bool; don't iteratorize a slice for fashion.

- Skill: `ctoth/go-iterators-rangefunc` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add ctoth/go-iterators-rangefunc`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ctoth/go-iterators-rangefunc/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: ctoth (https://skillmd.com/u/ctoth)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ctoth/go-iterators-rangefunc

---


# Go Iterators (Range-over-func)

> "An iterator is a function that passes successive elements of a sequence to a callback function, conventionally named yield. The function stops either when the sequence is finished or when yield returns false, indicating to stop the iteration early."
> — [iter package](https://pkg.go.dev/iter)

> "Yield panics if called after it returns false."
> — [iter package](https://pkg.go.dev/iter)

Since Go 1.23, `for x := range f` can range over a *function* when `f` is a push iterator. "The 'range' clause in a 'for-range' loop now accepts iterator functions of the following types ... as range expressions. Calls of the iterator argument function produce the iteration values for the 'for-range' loop" ([Go 1.23 release notes](https://go.dev/doc/go1.23)). The whole feature rests on one contract — `yield`'s `bool` return — and most of the bugs come from ignoring it. This skill owns `iter.Seq`/`Seq2`, writing push iterators, and `iter.Pull`; it routes the *data side* of the stdlib helpers to **[`go-slices-and-maps`](../go-slices-and-maps/SKILL.md)** and the generic mechanics to **[`go-generics`](../go-generics/SKILL.md)**.

---

## 1. The Shape: a Push Iterator Is a Function That Takes `yield`

An iterator is just a function. The `iter` package names the two shapes ([iter](https://pkg.go.dev/iter)):

```go
type Seq[V any]      func(yield func(V) bool)
type Seq2[K, V any]  func(yield func(K, V) bool)
```

`Seq[V]` yields one value per step; `Seq2[K, V]` yields a pair — "conventionally key-value or index-value pairs" ([iter](https://pkg.go.dev/iter)). You *write* an iterator by calling `yield` for each element; you *consume* one with `for ... range`:

```go
// Producer: a push iterator over a half-open range [lo, hi).
func Ints(lo, hi int) iter.Seq[int] {
	return func(yield func(int) bool) {
		for i := lo; i < hi; i++ {
			if !yield(i) { // honor the bool — stop the instant the consumer is done
				return
			}
		}
	}
}

// Consumer: range over the function value; the loop drives yield per iteration.
for v := range Ints(0, 3) { // 0, 1, 2
	fmt.Println(v)
}
```

The element type is a *type parameter* — an iterator is a generic seq type — so the generic-mechanics rules (constraints, inference) live in **[`go-generics`](../go-generics/SKILL.md)**; this skill is about the iteration contract.

---

## 2. The Rules and Their Sources

| Rule | The discipline | Source |
|---|---|---|
| Honor `yield`'s bool | Stop calling `yield` the moment it returns `false` | "Yield returns true if the iterator should continue ... false if it should stop" ([iter](https://pkg.go.dev/iter)) |
| Never yield after `false` | A `yield` call after it returned `false` is a runtime panic | "Yield panics if called after it returns false" ([iter](https://pkg.go.dev/iter)) |
| Clean up on stop | On early stop, do the cleanup the full run would have | "no more values are needed, and the iterator can just return, doing any cleanup that may be required" ([Range over func](https://go.dev/blog/range-functions)) |
| `range` carries control flow | `break`/`return`/labeled-break become `yield`→`false` | "A break statement would translate to `return false`" ([RangefuncExperiment](https://go.dev/wiki/RangefuncExperiment)) |
| `Pull` requires `stop` | Convert push→pull only with `defer stop()` | "Stop ends the iteration. It must be called when the caller is no longer interested in next values" ([iter](https://pkg.go.dev/iter)) |
| Don't iteratorize a slice | Already have a slice? Return it / range it directly | restraint rule, axis 2 ([`go-idiomatic-discipline`](../go-idiomatic-discipline/SKILL.md)) |
| Prefer stdlib producers | Use `slices.Values`/`maps.Keys`, don't hand-roll | "[Values](https://pkg.go.dev/slices#Values) returns an iterator over slice elements" ([Go 1.23](https://go.dev/doc/go1.23)) |
| `All` by convention | Name a collection's full-traversal iterator `All` | "The iterator method on a collection type is conventionally named All" ([iter](https://pkg.go.dev/iter)) |

---

## 3. The Headline Rule: Honor `yield`'s `bool` — or Panic

This is the contract, and ignoring it is *the* range-over-func bug. `yield` returns `false` to mean the consumer is finished — it broke, returned, or hit an error in the loop body. "If the yield function returns false, no more values are needed, and the iterator can just return" ([Range over func](https://go.dev/blog/range-functions)). The iterator **must** check that return and stop. Continuing to call `yield` after it returned `false` is not a soft bug — "Yield panics if called after it returns false" ([iter](https://pkg.go.dev/iter)). The runtime enforces it: "the yield function generated for the body checks if it is called after it has returned false or after the loop itself has exited. In either case, it will panic" ([RangefuncExperiment](https://go.dev/wiki/RangefuncExperiment)).

```go
func Lines(r io.Reader) iter.Seq[string] {
	return func(yield func(string) bool) {
		sc := bufio.NewScanner(r)
		for sc.Scan() {
			// WRONG:  yield(sc.Text())          // return dropped -> next yield after a break panics
			if !yield(sc.Text()) { // RIGHT — stop the instant yield returns false
				return // consumer is done; stop producing
			}
		}
	}
}
```

The mechanical idiom is always the same: `if !yield(v) { return }`. If your loop body is `yield(v)` with the result discarded, it is wrong.

---

## 4. `range` Translates `break` / `return` / `continue` Through `yield`

You don't write the bool-plumbing on the consumer side — the compiler does. A normal loop body falls through to an implicit `yield(...) == true`; the control-flow statements lower to a `false` return: "The `return true` at the end of the body is the implicit `continue` ... A break statement would translate to `return false` instead" ([RangefuncExperiment](https://go.dev/wiki/RangefuncExperiment)). A `return` out of the function, a labeled `break`, or a `goto` out of the loop also stop iteration — they "[require] setting a variable that the code outside the loop can consult when the loop breaks" ([RangefuncExperiment](https://go.dev/wiki/RangefuncExperiment)), but from the iterator's side it is the same signal: `yield` returns `false`. So in `for v := range seq { if v == 5 { break } }`, the `break` makes `seq`'s `yield(5)` return `false`; a `continue` is just the implicit `yield(...) == true`.

As a *producer* you only ever see the `bool`. As a *consumer* you write ordinary `break`/`continue`/`return` and they work — provided every iterator you call honors the contract in §3.

---

## 5. `iter.Pull` — Push to Pull, and You MUST Call `stop`

Range-over-func is a *push* model: the iterator drives. When you instead need to *pull* — step two sequences in lockstep, merge, peek — convert with `iter.Pull` ([iter](https://pkg.go.dev/iter)):

```go
func Pull[V any](seq Seq[V]) (next func() (V, bool), stop func())
func Pull2[K, V any](seq Seq2[K, V]) (next func() (K, V, bool), stop func())
```

`next` returns the following value and an "ok" bool; `stop` releases the iterator's resources (it runs on a goroutine under the hood). **Calling `stop` is mandatory** unless you drained to completion: "Stop ends the iteration. It must be called when the caller is no longer interested in next values and next has not yet signaled that the sequence is over (with a false boolean return). Typically, callers should `defer stop()`" ([iter](https://pkg.go.dev/iter)). Forgetting it leaks the underlying goroutine.

```go
// RIGHT — defer stop() the instant you Pull, so every return path cleans up
func first[V any](seq iter.Seq[V]) (V, bool) {
	next, stop := iter.Pull(seq)
	defer stop() // mandatory; a Pull without stop leaks the underlying goroutine
	return next()
}
```

Reach for `Pull` only when you genuinely need manual stepping; a plain `for range` is simpler and has nothing to forget.

---

## 6. Use the Stdlib Producers — Don't Hand-Roll `slices.Values`

Go 1.23 added iterator producers to `slices` and `maps`. Reach for them before writing your own ([Go 1.23 release notes](https://go.dev/doc/go1.23)):

```go
// slices: produce iterators
slices.Values(s)    // func Values[S ~[]E, E any](s S) iter.Seq[E]        — elements
slices.All(s)       // func All[S ~[]E, E any](s S) iter.Seq2[int, E]     — index, value
slices.Backward(s)  // iter.Seq2[int, E] — descending index
slices.Chunk(s, n)  // iter.Seq[S] — consecutive sub-slices of up to n

// slices: materialize / sort an iterator
slices.Collect(seq)       // func Collect[E any](seq iter.Seq[E]) []E      — empty -> nil
slices.Sorted(seq)        // collect, then sort (E cmp.Ordered)
slices.AppendSeq(s, seq)  // append a Seq to an existing slice

// maps: produce iterators (order unspecified, like map range) / materialize
maps.Keys(m)      // func Keys[M ~map[K]V, K comparable, V any](m M) iter.Seq[K]
maps.Values(m)    // iter.Seq[V]   ·   maps.All(m) // iter.Seq2[K, V]
maps.Collect(seq) // func Collect[K comparable, V any](seq iter.Seq2[K,V]) map[K]V
```

A common idiom: `keys := slices.Sorted(maps.Keys(m))` for an ordered key slice. `slices.Collect(seq)` "collects values from seq into a new slice ... If seq is empty, the result is nil" ([slices](https://pkg.go.dev/slices#Collect)) — the nil-vs-empty rule **[`go-slices-and-maps`](../go-slices-and-maps/SKILL.md)** owns; the generic `[S ~[]E, E any]` signature shape is **[`go-generics`](../go-generics/SKILL.md)**.

---

## 7. When NOT to Write an Iterator — Return the Slice You Already Have

This is the restraint rule, and it is axis 2 of **[`go-idiomatic-discipline`](../go-idiomatic-discipline/SKILL.md)** (over-abstraction). If you already hold a slice — or can cheaply build one — *return the slice*. An iterator buys you nothing over `[]T` you've already materialized, and it costs the reader a closure and the caller the inability to index, `len`, or re-range freely.

```go
// WRONG — wrapping an in-memory slice in an iterator for fashion
func (s *Store) Items() iter.Seq[Item] {
	return func(yield func(Item) bool) {
		for _, it := range s.items { // s.items is already a []Item
			if !yield(it) {
				return
			}
		}
	}
}

// RIGHT — you already have the slice; return it (callers range it directly)
func (s *Store) Items() []Item { return s.items }
```

Write an iterator when the sequence is **lazy, large, computed, streaming, or a custom container traversal** — when *not* materializing is the point: lines from an `io.Reader`, rows from a DB cursor, an infinite generator, a tree walk, a filtered/transformed pipeline. If the whole thing already fits in a slice in memory, a slice is the honest type. (For a slice you *do* want to hand out as a read-only sequence without copying, `slices.Values(s)` exists — but that is a deliberate choice, not the default.)

---

## 8. Composability and `Seq2` with a Trailing Error

Because an iterator is a function, you compose iterators by writing functions that take one `Seq` and return another — lazy `Filter`/`Map` without materializing:

```go
func Filter[V any](seq iter.Seq[V], keep func(V) bool) iter.Seq[V] {
	return func(yield func(V) bool) {
		for v := range seq {
			if keep(v) && !yield(v) { // honor the bool, as always
				return
			}
		}
	}
}

evens := Filter(slices.Values(nums), func(n int) bool { return n%2 == 0 })
for n := range evens { /* ... */ }
```

For sequences that can *fail* mid-iteration (a DB cursor, a network stream), the idiom is `iter.Seq2[V, error]` — yield `(value, nil)` per element and `(zero, err)` on failure, and the consumer checks the error each step:

```go
func Rows(q *sql.Rows) iter.Seq2[Record, error] {
	return func(yield func(Record, error) bool) {
		defer q.Close()
		for q.Next() {
			var r Record
			if err := q.Scan(&r.ID, &r.Name); err != nil {
				yield(Record{}, err) // surface the error; result tells us whether to keep going
				return
			}
			if !yield(r, nil) {
				return
			}
		}
		if err := q.Err(); err != nil {
			yield(Record{}, err)
		}
	}
}
// for rec, err := range Rows(rows) { if err != nil { return fmt.Errorf("rows: %w", err) }; use(rec) }
```

The consumer checks `err` each step. The `%w` wrapping and `errors.Is`/`As` discipline on that error live in **[`go-error-handling`](../go-error-handling/SKILL.md)**; this skill owns only the `Seq2`-with-error *shape*.

---

## 9. Performance: Iterators Are Closures — Fine, Not Free

A push iterator is a function value invoked per element, with `yield` a closure the compiler synthesizes. The Go team "put a fair bit of complexity in the Go compiler and runtime to make this efficient" ([Range over func](https://go.dev/blog/range-functions)) — for ordinary code the overhead is negligible and clarity wins, but it is not zero: in a measured hot path, ranging a plain `[]T` directly can beat an iterator wrapper. Don't pre-optimize; reach for the slice only when a profile names the iterator as the bottleneck. Measurement discipline is owned by **[`go-performance`](../go-performance/SKILL.md)**.

---

## 10. Routing to the Specific Skills

- **[`go-idiomatic-discipline`](../go-idiomatic-discipline/SKILL.md)** — the policy root. "Don't write an iterator for fashion" (§7) is its *axis 2*, over-abstraction; "clear is better than clever."
- **[`go-generics`](../go-generics/SKILL.md)** — `iter.Seq[V]` is a generic seq type; constraints, type inference, and the `[S ~[]E, E any]` stdlib signature shape live there.
- **[`go-slices-and-maps`](../go-slices-and-maps/SKILL.md)** — the *data side* of `slices.All`/`Values`/`Collect` and `maps.Keys`/`Values`/`Collect`: nil-vs-empty, ordering, backing arrays.
- **[`go-channels-select`](../go-channels-select/SKILL.md)** — the iterator-vs-channel choice: a `Seq` for in-process lazy sequences, a channel when crossing goroutines or fanning out.
- **[`go-error-handling`](../go-error-handling/SKILL.md)** — the `iter.Seq2[V, error]` pattern's error half: `%w` wrapping and `errors.Is`/`As`.
- **[`go-version-feature-map`](../go-version-feature-map/SKILL.md)** — range-over-func and `iter` are Go 1.23 (no longer `GOEXPERIMENT=rangefunc`); gate on the module's `go` directive.

---

## 11. Reference Files

High-frequency iterator anti-patterns in LLM-generated Go, each with wrong/right code and citations:

[references/common-mistakes.md](references/common-mistakes.md)

Source provenance for every claim in this skill:

[references/sources.yaml](references/sources.yaml)

