agentsclimarketplace

Go iterators rangefunc

Skill ctoth/golang-skills-plugin/plugins/golang/skills/go-iterators-rangefunc

Agent skill plugins for Go code quality and performance work.

Install
npx -y skills add ctoth/golang-skills-plugin --skill go-iterators-rangefunc

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

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.md

15.2 KB, as published. Nobody here has run it

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

"Yield panics if called after it returns false." — iter package

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). 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 and the generic mechanics to go-generics.


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):

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). You write an iterator by calling yield for each element; you consume one with for ... range:

// 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; this skill is about the iteration contract.


2. The Rules and Their Sources

RuleThe disciplineSource
Honor yield's boolStop calling yield the moment it returns false"Yield returns true if the iterator should continue ... false if it should stop" (iter)
Never yield after falseA yield call after it returned false is a runtime panic"Yield panics if called after it returns false" (iter)
Clean up on stopOn 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)
range carries control flowbreak/return/labeled-break become yieldfalse"A break statement would translate to return false" (RangefuncExperiment)
Pull requires stopConvert 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)
Don't iteratorize a sliceAlready have a slice? Return it / range it directlyrestraint rule, axis 2 (go-idiomatic-discipline)
Prefer stdlib producersUse slices.Values/maps.Keys, don't hand-roll"Values returns an iterator over slice elements" (Go 1.23)
All by conventionName a collection's full-traversal iterator All"The iterator method on a collection type is conventionally named All" (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). 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). 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).

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). 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), 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):

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). Forgetting it leaks the underlying goroutine.

// 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):

// 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) — the nil-vs-empty rule go-slices-and-maps owns; the generic [S ~[]E, E any] signature shape is go-generics.


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 (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.

// 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:

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:

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; 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) — 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.


10. Routing to the Specific Skills

  • go-idiomatic-discipline — the policy root. "Don't write an iterator for fashion" (§7) is its axis 2, over-abstraction; "clear is better than clever."
  • go-genericsiter.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 — the data side of slices.All/Values/Collect and maps.Keys/Values/Collect: nil-vs-empty, ordering, backing arrays.
  • go-channels-select — the iterator-vs-channel choice: a Seq for in-process lazy sequences, a channel when crossing goroutines or fanning out.
  • go-error-handling — the iter.Seq2[V, error] pattern's error half: %w wrapping and errors.Is/As.
  • go-version-feature-map — 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:

${CLAUDE_SKILL_DIR}/references/common-mistakes.md

Source provenance for every claim in this skill:

${CLAUDE_SKILL_DIR}/references/sources.yaml

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.