Iterator
Provide a way to access elements of a collection sequentially without exposing its underlying representation.
Go 1.23 made Iterator a first-class language feature: iter.Seq[T] (a function of the form func(yield func(T) bool)) integrates directly with for-range, replacing the channel-based and explicit Next()/Value() struct approaches that preceded it. Write the traversal once; every consumer gets a plain for v := range collection.InOrder() loop.
This is one of the patterns most transformed by Go's evolution — if you're on 1.23+, external iterator structs are rarely worth reaching for.
Problem
You have a binary tree and need to traverse it in multiple ways. Without an iterator abstraction, the traversal logic gets embedded in every function that processes the tree — search, print, collect, and filter all duplicate the same recursive walk.
duplicated_walk.gopackage tree import "fmt" type Node struct { Value int Left *Node Right *Node } // Every consumer duplicates the traversal logic. func PrintInOrder(n *Node) { if n == nil { return } PrintInOrder(n.Left) fmt.Println(n.Value) PrintInOrder(n.Right) } func SumInOrder(n *Node) int { if n == nil { return 0 } return SumInOrder(n.Left) + n.Value + SumInOrder(n.Right) } func CollectInOrder(n *Node) []int { if n == nil { return nil } result := CollectInOrder(n.Left) result = append(result, n.Value) result = append(result, CollectInOrder(n.Right)...) return result } // Same walk, three copies. Adding pre-order multiplies this again.
The traversal logic (go left, visit, go right) is copy-pasted into every function that needs to process the tree. Adding a new traversal order means duplicating all the processing functions.
Solution
With Go 1.23's range-over-func, define an iterator that yields values. Consumers use a plain for-range loop — the traversal logic is written once.
text┌─────────────┐ │ Node.All() │──► iter.Seq[int] └──────┬──────┘ │ for v := range node.InOrder() { // v is each value, in order }
tree.gopackage tree import "iter" type Node struct { Value int Left *Node Right *Node } // InOrder returns an iterator over the tree's values in sorted order. // iter.Seq[int] is func(yield func(int) bool). func (n *Node) InOrder() iter.Seq[int] { return func(yield func(int) bool) { if n == nil { return } for v := range n.Left.InOrder() { if !yield(v) { return } } if !yield(n.Value) { return } for v := range n.Right.InOrder() { if !yield(v) { return } } } }
Consumers use a plain for-range — no iterator boilerplate:
main.gopackage main import ( "fmt" "tree" ) func main() { root := &tree.Node{ Value: 4, Left: &tree.Node{ Value: 2, Left: &tree.Node{Value: 1}, Right: &tree.Node{Value: 3}, }, Right: &tree.Node{ Value: 6, Left: &tree.Node{Value: 5}, Right: &tree.Node{Value: 7}, }, } fmt.Print("In-order: ") for v := range root.InOrder() { fmt.Printf("%d ", v) } fmt.Println() sum := 0 for v := range root.InOrder() { sum += v } fmt.Println("Sum:", sum) fmt.Print("First 3: ") count := 0 for v := range root.InOrder() { fmt.Printf("%d ", v) count++ if count == 3 { break } } fmt.Println() }
Output:
textIn-order: 1 2 3 4 5 6 7 Sum: 28 First 3: 1 2 3
For simple collections, returning a
[]Tslice is perfectly idiomatic Go. Only reach foriter.Seqwhen you need lazy evaluation, custom traversal orders, or iteration over structures where materializing all values would be expensive.
When to Use
- You need to traverse a data structure without exposing its internals.
- Multiple consumers need different processing of the same traversal.
- You want lazy evaluation — don't build a full slice when you only need the first few elements.
- You're on Go 1.23+ — use
iter.Seq[T]as the primary approach.
When Not to Use
- A simple slice covers your needs.
[]Twith a for-range loop is the simplest iterator. - The collection is small and fits in memory — just return a slice from a method.
- You need bidirectional iteration (prev/next) —
iter.Seqdoesn't support this naturally.
Tradeoffs
The range-over-func form integrates cleanly with Go's syntax and handles early termination via break naturally — the yield return value propagates the stop signal up the call stack. The main cost is that recursive iterators, like tree traversal, carry goroutine-free stack frames for each level of nesting, which adds overhead compared to a plain recursive function materializing a slice. Before Go 1.23, channel-based iterators were the workaround, but they leak goroutines if the consumer breaks early without draining the channel — a real production bug waiting to happen. The iter.Seq approach eliminates that hazard entirely. The trade-off that remains: if you need two-pointer traversal or bidirectional iteration, you'll need to materialize a slice or build an explicit cursor struct.
Related Patterns
- Composite — Iterator is the natural way to traverse a Composite tree without exposing its structure; the traversal logic is written once in the iterator and all consumers use for-range.
- Visitor — Iterator provides sequential access to elements; Visitor performs type-specific operations on each element — combine them when you need to traverse a tree and apply different logic per node type.