{} The Go Reference

Behavioral pattern · Gang of Four · Beginner

Iterator

Provide a way to access the elements of a collection sequentially without exposing its underlying representation.

Also known as — Cursor

Behavioral Beginner ⏱ 4 min read Complete

📺 Analogy

A TV remote’s “channel up” button steps you through channels one at a time. You never need to know whether channels are stored in an array, a linked list, or fetched over the network — you just keep pressing “next” until there’s nothing left. That button is an iterator.

The problem

Different collections store their elements differently — a menu in a slice, a filesystem in a tree, query results in a database cursor. If callers loop using each collection’s internal shape, they’re coupled to that shape. Iterator gives every collection the same traversal interface, so the container can change internally without breaking the code that walks it.

Structure

classDiagram
class Iterator {
  <<interface>>
  +HasNext() bool
  +Next() string
}
class MenuIterator {
  -items string[]
  -pos int
  +HasNext() bool
  +Next() string
}
Iterator <|.. MenuIterator

Idiomatic Go — the classic form

The classic GoF form is a small Iterator interface with HasNext/Next. (Returning a concrete type beats interface{} so callers skip type assertions.) Edit and Run:

iterator.go — editable & runnable
package main

import "fmt"

// Iterator: a uniform way to walk a collection.
type Iterator interface {
HasNext() bool
Next() string
}

type MenuIterator struct {
items []string
pos   int
}

func (m *MenuIterator) HasNext() bool { return m.pos < len(m.items) }
func (m *MenuIterator) Next() string {
item := m.items[m.pos]
m.pos++
return item
}

func main() {
it := &MenuIterator{items: []string{"Pizza", "Burger", "Risotto"}}
for it.HasNext() {
	fmt.Println(it.Next())
}
}

Idiomatic Go — range-over-func (Go 1.23+)

Modern Go makes custom iteration first-class. A function of type iter.Seq[T] can be ranged over directly — no HasNext/Next, no exposed index:

import "iter"

func Menu() iter.Seq[string] {
	items := []string{"Pizza", "Burger", "Risotto"}
	return func(yield func(string) bool) {
		for _, item := range items {
			if !yield(item) { // stop early if the caller breaks
				return
			}
		}
	}
}

func main() {
	for dish := range Menu() { // looks just like ranging a slice
		fmt.Println(dish)
	}
}

🐹 Go gives you three iterators

For a plain slice or map, just use for range. For custom sequences, prefer range-over-func (iter.Seq). Reach for the classic HasNext/Next interface only when you need an explicit cursor object you can pass around or pause. Channels are a fourth, concurrent option — see Generator.

Internal vs external — and Single Responsibility

GoF and Head First split iterators two ways:

  • External (active) — the caller drives: for it.HasNext() { it.Next() }. More control (pause, interleave two walks, hand the cursor around).
  • Internal (passive) — the iterator drives and pushes each element to a callback. That’s exactly Go 1.23 range-over-func: the iter.Seq function runs the loop and calls yield. Less boilerplate, hides more.

Either way, the deeper point is the Single Responsibility Principle: storing elements and traversing them are two responsibilities. Bake every traversal into the collection and it has many reasons to change; extract traversal into an iterator and each type does one job — which is also what lets several independent walks run over the same data at once.

In the standard library

  • bufio.ScannerScan() / Text() walk tokens without exposing the buffer.
  • sql.RowsNext() / Scan() iterate query results.
  • slices.All, maps.Keys, strings.Lines (Go 1.23+) — return iter.Seq values.

Pitfalls

⚠️ Don't out-engineer a slice

If your data is already a slice, a custom iterator adds indirection for nothing — for range is the idiomatic answer. Build an iterator when the structure is non-trivial (a tree, a paged API, a stream) or when you genuinely need to hide it.

When to use it — and when not

✅ Reach for it when

  • Callers should traverse a collection without knowing whether it's a slice, tree, ring buffer, or DB cursor.
  • You want one uniform traversal API across different container types.
  • You need more than one independent walk over the same data at once.

⛔ Think twice when

  • It's a plain slice or map — a `for range` is simpler than any iterator machinery.
  • You'd be building an iterator framework where the language already gives you one.

Check your understanding

Score: 0 / 5

1. What does the Iterator pattern hide from the caller?

The caller uses HasNext/Next (or `range`) without depending on how the collection stores its elements, so the container can change internally without breaking callers.

2. What is Go's modern, built-in form of Iterator (Go 1.23+)?

Go 1.23 added range-over-func: a function of type iter.Seq[T] can be ranged over directly, making custom iteration first-class without HasNext/Next boilerplate.

3. Which is a standard-library iterator?

bufio.Scanner walks tokens (lines, words) one at a time via Scan()/Text() without revealing the buffer underneath — a textbook iterator. sql.Rows is another.

4. Internal vs external iterator — which is range-over-func?

An external iterator (HasNext/Next) puts the caller in charge of advancing. An internal iterator owns the iteration and pushes each element to a callback — exactly range-over-func, where the iter.Seq function runs the loop and invokes yield. Internal hides more; external gives finer control (pause, interleave).

5. Which design principle says iteration belongs in its own object, not baked into the collection?

If the collection both stores elements and implements every way to traverse them, it has many reasons to change. Extracting traversal into an iterator gives each type a single responsibility — and lets multiple independent walks coexist.

Comments

Sign in with GitHub to join the discussion.