📺 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 <|.. MenuIteratorIdiomatic 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:
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.Seqfunction runs the loop and callsyield. 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.Scanner—Scan()/Text()walk tokens without exposing the buffer.sql.Rows—Next()/Scan()iterate query results.slices.All,maps.Keys,strings.Lines(Go 1.23+) — returniter.Seqvalues.
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.
Related patterns
Treat individual objects and compositions of objects uniformly through one common interface.
concurrencyGeneratorProduce a stream of values from a goroutine over a channel, lazily and on demand.
behavioralVisitorAdd new operations to a set of object types without modifying those types, by moving each operation into a visitor.
Check your understanding
Score: 0 / 51. 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.