Get AI summaries of any video or article — Sign up free
Go Iterators Are Bad thumbnail

Go Iterators Are Bad

The PrimeTime·
5 min read

Based on The PrimeTime's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.

TL;DR

Go 1.23 iterators are controversial mainly because range loops are rewritten into nested callback-style functions with yield-driven control flow.

Briefing

Go 1.23’s new iterator design has sparked backlash because it makes Go feel more “functional” and syntactically magical than many programmers expect from an unapologetically imperative language. The core friction centers on how iteration is expressed: range loops are transformed into nested function calls with “yield”-style control flow, and breaks/escape paths get rewritten into boolean returns. Supporters like the generator-like feel and the ability to run cleanup via defer inside the iterator’s control flow, but critics say the resulting syntax and semantics are harder to read, harder to reason about, and more complex than Go’s usual “simple and explicit” style.

A concrete example from the proposal shows curried functions and a “yield” callback used to drive iteration, including the ability to range backward. While the example is readable in isolation, the broader design is criticized for being “a lot of effing syntax” to express what many consider a basic imperative loop. One proposed equivalence illustrates the transformation: the range body becomes a function that returns a boolean (continue vs stop), and “break” effectively turns into returning false. That rewrite is part of why opponents describe the feature as turning imperative control flow into a functional callback pipeline.

The discussion also splits over iterator implementation strategy—“push” vs “pull.” Russ Cox’s rationale (as referenced in the conversation) favors storing state inside the iterator’s control flow so that cleanup and minimal stack-frame sharing work naturally, and it argues that push iterators are often more convenient to implement. Critics counter that pull-style iterators can be easier to understand and that storing state outside the control flow—such as in a struct with explicit fields—matches how many programmers already model iterators in languages like Rust. That struct-based approach is repeatedly framed as more “one-dimensional,” easier to implement mentally, and less dependent on nested function layers.

Beyond readability, performance and ergonomics concerns surface. Some worry that closures and callbacks—central to the generator-like design—could be a poor fit for Go’s typical performance expectations, even if the feature is mostly used rather than implemented by most developers. Others argue the standard library and third-party packages will shoulder most iterator complexity, so the average Go programmer will mostly consume iterators rather than build them.

The conversation broadens into language-design philosophy: Go is designed for broad adoption, not for researchers or highly specialized language tinkerers. Critics say the iterator design feels out of character for Go’s “grug” audience—people who want straightforward, patchable code rather than clever syntax. Supporters emphasize that iterators unlock useful patterns and align with Go’s evolving ecosystem (generics, reusable slice/map utilities). Still, the loudest takeaway is cultural: even when the design choices are defensible, the resulting syntax and control-flow rewriting make Go 1.23 iterators feel like a departure from what many people think Go should be.

Cornell Notes

Go 1.23’s iterator proposal is drawing anger because it makes Go’s range loops behave like generator-style callbacks: the loop body gets wrapped into nested functions, and control-flow exits like break are rewritten into boolean returns. Supporters like the generator feel and the way defer-based cleanup can fit naturally into the iterator’s control flow. Critics say the syntax is too “functional” for an imperative language and that the semantics are harder to read—especially when the design relies on closures and yield-style plumbing. The debate also contrasts “push” vs “pull” iterators, with opponents preferring struct-based, state-in-a-field iterators (closer to Rust) over control-flow-driven state (closer to generator transforms).

What specific mechanism makes Go 1.23 iterators feel “functional” to critics?

Range loops are transformed into callback-style function layers. The iterator design uses a yield-driven function that passes values to a loop callback; breaks and other escape control flows are converted into returning false (a boolean “continue/stop” signal). That means imperative control flow is effectively routed through nested functional plumbing rather than staying as a straightforward loop body.

Why do some people prefer “pull” iterators over “push” iterators in this discussion?

Pull iterators are described as producing lazy values: the consumer asks for the next item, and the iterator yields one value at a time through filter/map-like steps. Push iterators are compared to eager execution pipelines (e.g., JavaScript map/filter running across the whole array before forEach), which can require buffering and extra copying. In the conversation, pull is framed as easier to understand because it processes values incrementally as the loop requests them.

How does the debate about state placement (control flow vs struct fields) shape the criticism?

Russ Cox’s approach is praised for storing data in the iterator’s control flow, enabling cleanup and minimizing stack-frame sharing. Critics argue that storing state outside the control flow—such as in a struct that holds iterator state—feels more straightforward and “one-dimensional.” They claim struct-based iterators are easier to implement and reason about, because the state is explicit and the next/stop logic is localized.

What role does defer-based cleanup play in the pro-iterator argument?

Supporters like that cleanup can be handled cleanly with defer inside the iterator’s control flow. The design goal is to make iterators look like generators while still allowing setup/teardown behavior around yield points, reducing the need for manual resource management spread across multiple operations.

Why do some participants think most Go programmers won’t be harmed even if iterators are complex?

A recurring point is that many iterators will already exist in the standard library or popular third-party packages. That shifts the burden of implementing the tricky iterator machinery onto package authors, while most users simply consume iterators via range loops.

What broader language-philosophy argument appears in the backlash?

Go is repeatedly characterized as an imperative, broadly accessible language designed for “mediocre” (i.e., typical) programmers who want clarity and easy refactoring. Critics say the iterator syntax and semantics feel too clever or magical—especially compared with Go’s usual approach to hiding less and keeping constructs understandable—so the feature feels out of character.

Review Questions

  1. Which transformation described in the discussion turns break-like control flow into a boolean return, and why does that matter for readability?
  2. Compare the struct-based iterator approach and the control-flow/yield-based approach: what does each make easier or harder to reason about?
  3. How do the push vs pull comparisons relate to laziness, buffering, and copying in the examples given?

Key Points

  1. 1

    Go 1.23 iterators are controversial mainly because range loops are rewritten into nested callback-style functions with yield-driven control flow.

  2. 2

    Break and other escape paths are effectively converted into boolean returns (continue vs stop), which critics see as functional-style plumbing in an imperative language.

  3. 3

    Supporters value generator-like behavior and the ability to use defer for cleanup naturally within iterator control flow.

  4. 4

    The push vs pull debate centers on laziness and incremental consumption: pull is framed as more efficient and easier to understand than push pipelines that may buffer or copy.

  5. 5

    A major readability dispute is whether iterator state should live in control flow (generator transform) or in explicit struct fields (state-machine style).

  6. 6

    Some participants argue complexity is acceptable because most developers will consume iterators from the standard library or third-party packages rather than implement them.

  7. 7

    The backlash also reflects Go’s cultural design goal: keep features approachable and explicit for typical programmers rather than “magical” cleverness.

Highlights

The loudest complaint is semantic: range loops become callback functions, and break-like exits are rewritten into returning false.
Cleanup is one of the strongest pro-arguments—defer can fit into the iterator’s control flow without awkward manual teardown scaffolding.
Critics repeatedly prefer struct-based iterators with explicit state, calling the control-flow/yield approach harder to read and mentally model.
The push vs pull split is framed as eager pipeline vs lazy, incremental consumption, with pull generally favored for clarity.

Topics

  • Go Iterators
  • Generator Semantics
  • Push vs Pull
  • Control-Flow Cleanup
  • Language Design Philosophy

Mentioned

  • Russ Cox