Iterator Pattern #

A ProductCatalog stores products in a tree structure — categories have sub-categories, sub-categories have products. The code that needs to process all products should not care whether the data is stored as a flat slice, a tree, a linked list, or a streamed database query result. The Iterator Pattern separates two things that are often mixed together: how data is stored and how data is traversed. The collection provides an Iterator; the client uses the Iterator; nobody needs to know the internal storage details. If the storage structure ever changes from a slice to a database, the client does not need to change — only the Iterator implementation does.

What Is the Iterator Pattern? #

The Iterator Pattern is a behavioral design pattern that provides a way to access the elements of a collection sequentially without exposing its internal representation. The pattern separates traversal logic (how to traverse) from the data structure (how to store), so the two can evolve independently.

Three properties define the Iterator Pattern:

  • Traversal encapsulation — the client does not need to know whether the collection is a slice, tree, graph, or database query result
  • Multiple iterators — several traversals can run simultaneously on the same collection without interfering with each other
  • Lazy evaluation — elements can be generated on demand rather than all at once; this enables iterating over very large datasets
flowchart LR
    subgraph "Without Iterator"
        C1[Client] -->|"knows internal structure"| SL["slice: catalog.products[i]"]
        C1 -->|"knows how to traverse tree"| TR["tree: recursiveWalk(root)"]
        C1 -->|"knows how to query DB"| DB["db: rows.Next(), rows.Scan()"]
    end

    subgraph "With Iterator"
        C2[Client] -->|"it.HasNext() / it.Next()"| IT[Iterator\\ninterface]
        IT --> SL2[SliceIterator]
        IT --> TR2[TreeIterator]
        IT --> DB2[DBIterator]
    end

Iterators in Go: More Than One Way #

Go has several idiomatic ways to implement an Iterator, each with different trade-offs.

Way 1: Classic Interface (HasNext/Next) #

// Classic Iterator interface — the most explicit, closest to traditional OOP
type Iterator[T any] interface {
    HasNext() bool
    Next() T
    Reset()
}

Way 2: Channel-based (idiomatic Go) #

// Channel-based iterator — natural for Go, supports range
func Iterate(collection []Product) <-chan Product {
    ch := make(chan Product)
    go func() {
        defer close(ch)
        for _, p := range collection {
            ch <- p
        }
    }()
    return ch
}

// Usage with range
for product := range Iterate(catalog.Products()) {
    fmt.Println(product.Name)
}

Way 3: Functional Iterator (Go 1.23+) #

// iter.Seq is the type introduced in Go 1.23
// func(yield func(T) bool)
func (c *ProductCatalog) All() iter.Seq[Product] {
    return func(yield func(Product) bool) {
        for _, p := range c.products {
            if !yield(p) {
                return // yield returning false = stop iterating
            }
        }
    }
}

// Usage with range (Go 1.23+)
for product := range catalog.All() {
    fmt.Println(product.Name)
}

Way 4: Callback-based #

// Callback-based iterator — simple for small collections
func (c *ProductCatalog) ForEach(fn func(product Product) bool) {
    for _, p := range c.products {
        if !fn(p) {
            break // fn returning false = stop
        }
    }
}

// Usage
catalog.ForEach(func(p Product) bool {
    if p.Price > 1000000 {
        return false // stop at the first expensive product
    }
    fmt.Println(p.Name)
    return true
})

Comparing the four approaches:

ApproachProsConsBest For
HasNext/Next interfaceFamiliar, explicit, easy to mockVerbose, cannot use rangeLibraries that many developers need to understand
Channel-basedNatural Go, uses rangeGoroutine overhead, needs context for cancellationConcurrent iteration, pipelines
Functional (iter.Seq)Most idiomatic Go 1.23+, uses rangeGo 1.23+ onlyModern codebases
CallbackSimplestLazy stop is awkwardSmall collections, internal use

Full Implementation: Product Catalog with Multiple Iterators #

Collection and Iterator Interfaces #

package catalog

// Product represents one product in the catalog.
type Product struct {
    ID       string
    Name     string
    Category string
    Price    float64
    InStock  bool
    Tags     []string
}

// ProductIterator is the interface for iterating products.
// Uses the classic interface approach for compatibility with all Go versions.
type ProductIterator interface {
    HasNext() bool
    Next() *Product
    Reset()
}

// ProductCollection is the Aggregate interface — a collection that can create iterators.
type ProductCollection interface {
    CreateIterator() ProductIterator
    CreateFilteredIterator(predicate func(*Product) bool) ProductIterator
    CreateSortedIterator(less func(a, b *Product) bool) ProductIterator
    Count() int
}

Concrete Aggregate: ProductCatalog #

package catalog

import (
    "sort"
)

// ProductCatalog is the Concrete Aggregate — it stores products in a slice.
type ProductCatalog struct {
    products []*Product
}

func NewProductCatalog() *ProductCatalog {
    return &ProductCatalog{products: make([]*Product, 0)}
}

// Add adds a product to the catalog.
func (c *ProductCatalog) Add(product *Product) {
    c.products = append(c.products, product)
}

// Count returns the number of products in the catalog.
func (c *ProductCatalog) Count() int { return len(c.products) }

// CreateIterator creates a basic iterator that traverses all products.
func (c *ProductCatalog) CreateIterator() ProductIterator {
    return &SliceIterator{
        products: c.products,
        index:    0,
    }
}

// CreateFilteredIterator creates an iterator that only returns products
// satisfying a given predicate.
func (c *ProductCatalog) CreateFilteredIterator(predicate func(*Product) bool) ProductIterator {
    return &FilteredIterator{
        source:    c.CreateIterator(),
        predicate: predicate,
    }
}

// CreateSortedIterator creates an iterator that returns products in a given order.
// Makes a copy of the slice to avoid modifying the original collection.
func (c *ProductCatalog) CreateSortedIterator(less func(a, b *Product) bool) ProductIterator {
    sorted := make([]*Product, len(c.products))
    copy(sorted, c.products)
    sort.Slice(sorted, func(i, j int) bool {
        return less(sorted[i], sorted[j])
    })
    return &SliceIterator{products: sorted, index: 0}
}

// InStockIterator shortcut: iterator for in-stock products only.
func (c *ProductCatalog) InStockIterator() ProductIterator {
    return c.CreateFilteredIterator(func(p *Product) bool {
        return p.InStock
    })
}

// CategoryIterator shortcut: iterator for a specific category.
func (c *ProductCatalog) CategoryIterator(category string) ProductIterator {
    return c.CreateFilteredIterator(func(p *Product) bool {
        return p.Category == category
    })
}

// PriceRangeIterator shortcut: iterator for a specific price range.
func (c *ProductCatalog) PriceRangeIterator(minPrice, maxPrice float64) ProductIterator {
    return c.CreateFilteredIterator(func(p *Product) bool {
        return p.Price >= minPrice && p.Price <= maxPrice
    })
}

Concrete Iterators #

package catalog

// SliceIterator traverses products in a slice sequentially.
type SliceIterator struct {
    products []*Product
    index    int
}

func (it *SliceIterator) HasNext() bool {
    return it.index < len(it.products)
}

func (it *SliceIterator) Next() *Product {
    if !it.HasNext() {
        return nil
    }
    product := it.products[it.index]
    it.index++
    return product
}

func (it *SliceIterator) Reset() {
    it.index = 0
}


// FilteredIterator wraps another iterator and only returns
// elements satisfying a predicate. This is a Decorator on the Iterator.
type FilteredIterator struct {
    source    ProductIterator
    predicate func(*Product) bool
    next      *Product // the next element already prefetched
    fetched   bool     // whether prefetch has happened
}

func (it *FilteredIterator) prefetch() {
    for it.source.HasNext() {
        product := it.source.Next()
        if it.predicate(product) {
            it.next = product
            it.fetched = true
            return
        }
    }
    it.next = nil
    it.fetched = true
}

func (it *FilteredIterator) HasNext() bool {
    if !it.fetched {
        it.prefetch()
    }
    return it.next != nil
}

func (it *FilteredIterator) Next() *Product {
    if !it.fetched {
        it.prefetch()
    }
    result := it.next
    it.fetched = false
    it.next = nil
    return result
}

func (it *FilteredIterator) Reset() {
    it.source.Reset()
    it.next = nil
    it.fetched = false
}


// ReverseIterator traverses the slice from back to front.
type ReverseIterator struct {
    products []*Product
    index    int
}

func NewReverseIterator(products []*Product) ProductIterator {
    return &ReverseIterator{
        products: products,
        index:    len(products) - 1,
    }
}

func (it *ReverseIterator) HasNext() bool {
    return it.index >= 0
}

func (it *ReverseIterator) Next() *Product {
    if !it.HasNext() {
        return nil
    }
    product := it.products[it.index]
    it.index--
    return product
}

func (it *ReverseIterator) Reset() {
    it.index = len(it.products) - 1
}

Usage: The Client Does Not Know the Internal Structure #

func main() {
    catalog := catalog.NewProductCatalog()

    catalog.Add(&catalog.Product{ID: "P001", Name: "Laptop Pro", Category: "Electronics",
        Price: 15000000, InStock: true, Tags: []string{"laptop", "premium"}})
    catalog.Add(&catalog.Product{ID: "P002", Name: "Wireless Mouse", Category: "Electronics",
        Price: 350000, InStock: true, Tags: []string{"mouse", "wireless"}})
    catalog.Add(&catalog.Product{ID: "P003", Name: "Standing Desk", Category: "Furniture",
        Price: 5000000, InStock: false, Tags: []string{"desk", "ergonomic"}})
    catalog.Add(&catalog.Product{ID: "P004", Name: "Mechanical Keyboard", Category: "Electronics",
        Price: 2500000, InStock: true, Tags: []string{"keyboard", "mechanical"}})

    // Basic iterator: all products
    fmt.Println("=== All Products ===")
    it := catalog.CreateIterator()
    for it.HasNext() {
        p := it.Next()
        fmt.Printf("  %s: Rp %.0f\n", p.Name, p.Price)
    }

    // Iterator with a filter: only in-stock items
    fmt.Println("\n=== In-Stock Products ===")
    it = catalog.InStockIterator()
    for it.HasNext() {
        p := it.Next()
        fmt.Printf("  %s\n", p.Name)
    }

    // Iterator with sorting: cheapest first
    fmt.Println("\n=== Sorted by Price (Cheapest) ===")
    it = catalog.CreateSortedIterator(func(a, b *catalog.Product) bool {
        return a.Price < b.Price
    })
    for it.HasNext() {
        p := it.Next()
        fmt.Printf("  %s: Rp %.0f\n", p.Name, p.Price)
    }

    // Multiple simultaneous iterators — they do not interfere
    fmt.Println("\n=== Multiple Simultaneous Iterators ===")
    it1 := catalog.CreateIterator()
    it2 := catalog.CreateIterator()
    _ = it1.Next() // it1 advances one step
    fmt.Printf("it1 next: %s\n", it1.Next().Name)
    fmt.Printf("it2 next: %s\n", it2.Next().Name) // it2 is still at the start
}

Tree Iterator: Recursive Traversal #

For tree structures (like file systems or nested product categories), the Iterator hides the recursive traversal complexity from the client.

package tree

// CategoryNode represents a node in the category tree.
type CategoryNode struct {
    Name     string
    Products []Product
    Children []*CategoryNode
}

// DFSIterator performs depth-first search traversal on the category tree.
// The client does not need to know this is a tree — just use HasNext/Next.
type DFSIterator struct {
    stack    []*CategoryNode
    products []Product // product buffer from the current node
    prodIdx  int
}

func NewDFSIterator(root *CategoryNode) *DFSIterator {
    it := &DFSIterator{
        stack: make([]*CategoryNode, 0),
    }
    it.stack = append(it.stack, root)
    it.loadNextNode()
    return it
}

func (it *DFSIterator) loadNextNode() {
    it.products = nil
    it.prodIdx = 0

    for len(it.stack) > 0 && len(it.products) == 0 {
        // Pop from the stack
        node := it.stack[len(it.stack)-1]
        it.stack = it.stack[:len(it.stack)-1]

        // Push children onto the stack (in reverse order for correct DFS)
        for i := len(node.Children) - 1; i >= 0; i-- {
            it.stack = append(it.stack, node.Children[i])
        }

        it.products = node.Products
    }
}

func (it *DFSIterator) HasNext() bool {
    return it.prodIdx < len(it.products)
}

func (it *DFSIterator) Next() Product {
    if !it.HasNext() {
        return Product{}
    }
    p := it.products[it.prodIdx]
    it.prodIdx++
    if it.prodIdx >= len(it.products) {
        it.loadNextNode()
    }
    return p
}

func (it *DFSIterator) Reset() {
    // Reset is not easy for a tree iterator — create a new iterator
    panic("DFSIterator does not support Reset — create a new iterator")
}


// BFSIterator performs breadth-first search traversal.
type BFSIterator struct {
    queue    []*CategoryNode
    products []Product
    prodIdx  int
}

func NewBFSIterator(root *CategoryNode) *BFSIterator {
    it := &BFSIterator{
        queue: []*CategoryNode{root},
    }
    it.loadNextNode()
    return it
}

func (it *BFSIterator) loadNextNode() {
    it.products = nil
    it.prodIdx = 0

    for len(it.queue) > 0 && len(it.products) == 0 {
        node := it.queue[0]
        it.queue = it.queue[1:]
        it.queue = append(it.queue, node.Children...)
        it.products = node.Products
    }
}

func (it *BFSIterator) HasNext() bool { return it.prodIdx < len(it.products) }

func (it *BFSIterator) Next() Product {
    if !it.HasNext() {
        return Product{}
    }
    p := it.products[it.prodIdx]
    it.prodIdx++
    if it.prodIdx >= len(it.products) {
        it.loadNextNode()
    }
    return p
}

func (it *BFSIterator) Reset() {
    panic("BFSIterator does not support Reset — create a new iterator")
}

Database Cursor as an Iterator #

The Iterator Pattern is very natural for database cursors — each row is only loaded on demand, not all at once.

package dbiter

import (
    "context"
    "database/sql"
    "fmt"
)

// UserIterator iterates over database rows lazily — one row per Next().
// Not all rows are loaded into memory at once.
type UserIterator struct {
    rows    *sql.Rows
    current *User
    err     error
    done    bool
}

// User represents one row from the users table.
type User struct {
    ID    int
    Name  string
    Email string
    Role  string
}

// NewUserIterator creates an iterator from a database query.
func NewUserIterator(ctx context.Context, db *sql.DB, query string, args ...interface{}) (*UserIterator, error) {
    rows, err := db.QueryContext(ctx, query, args...)
    if err != nil {
        return nil, fmt.Errorf("query failed: %w", err)
    }
    return &UserIterator{rows: rows}, nil
}

// HasNext checks whether there is another row.
// Internally, this prefetches the next row.
func (it *UserIterator) HasNext() bool {
    if it.done {
        return false
    }
    if !it.rows.Next() {
        it.done = true
        it.rows.Close()
        return false
    }
    var u User
    if err := it.rows.Scan(&u.ID, &u.Name, &u.Email, &u.Role); err != nil {
        it.err = err
        it.done = true
        return false
    }
    it.current = &u
    return true
}

// Next returns the current user (already prefetched by HasNext).
func (it *UserIterator) Next() *User {
    return it.current
}

// Error returns the last error, if any.
func (it *UserIterator) Error() error { return it.err }

// Close closes the database rows.
func (it *UserIterator) Close() error { return it.rows.Close() }

// Usage
func processAllUsers(ctx context.Context, db *sql.DB) error {
    it, err := NewUserIterator(ctx, db,
        "SELECT id, name, email, role FROM users WHERE is_active = true ORDER BY id")
    if err != nil {
        return err
    }
    defer it.Close()

    for it.HasNext() {
        user := it.Next()
        // Process one user at a time — not all of them in memory
        fmt.Printf("Processing: %s (%s)\n", user.Name, user.Email)
    }

    return it.Error()
}

Channel-based Iterator for Concurrent Processing #

Channel-based iterators are a great fit when data is produced asynchronously or needs concurrent processing.

package pipeline

import (
    "context"
    "fmt"
)

// StreamProducts produces products one by one through a channel.
// Consumers can read products without knowing how they are generated.
func StreamProducts(ctx context.Context, catalog *ProductCatalog) <-chan *Product {
    ch := make(chan *Product, 10) // buffer to reduce blocking
    go func() {
        defer close(ch)
        it := catalog.CreateIterator()
        for it.HasNext() {
            select {
            case <-ctx.Done():
                return // context cancelled — stop streaming
            case ch <- it.Next():
                // product delivered to the consumer
            }
        }
    }()
    return ch
}

// FilterStream wraps a channel stream with a filter — a Decorator on the channel iterator.
func FilterStream(ctx context.Context, in <-chan *Product, predicate func(*Product) bool) <-chan *Product {
    out := make(chan *Product, 10)
    go func() {
        defer close(out)
        for {
            select {
            case <-ctx.Done():
                return
            case p, ok := <-in:
                if !ok {
                    return
                }
                if predicate(p) {
                    out <- p
                }
            }
        }
    }()
    return out
}

// Usage: pipeline with channel iterators
func buildPipeline(ctx context.Context, catalog *ProductCatalog) {
    // Stream all products
    all := StreamProducts(ctx, catalog)

    // Filter only in-stock items
    inStock := FilterStream(ctx, all, func(p *Product) bool {
        return p.InStock
    })

    // Filter only items under 5 million
    affordable := FilterStream(ctx, inStock, func(p *Product) bool {
        return p.Price < 5000000
    })

    // Consumer — process the pipeline result
    for product := range affordable {
        fmt.Printf("Affordable in-stock: %s (Rp %.0f)\n", product.Name, product.Price)
    }
}

Iterators with Helper Functions #

Helper functions that work with generic iterators make client code more expressive.

// Collect gathers all elements from an iterator into a slice.
func Collect(it ProductIterator) []*Product {
    var results []*Product
    for it.HasNext() {
        results = append(results, it.Next())
    }
    return results
}

// First returns the first element satisfying the predicate.
func First(it ProductIterator, predicate func(*Product) bool) *Product {
    for it.HasNext() {
        p := it.Next()
        if predicate(p) {
            return p
        }
    }
    return nil
}

// Count counts the elements satisfying the predicate.
func Count(it ProductIterator, predicate func(*Product) bool) int {
    count := 0
    for it.HasNext() {
        if predicate(it.Next()) {
            count++
        }
    }
    return count
}

// Reduce aggregates all elements into a single value.
func Reduce[T any](it ProductIterator, initial T, fn func(acc T, p *Product) T) T {
    acc := initial
    for it.HasNext() {
        acc = fn(acc, it.Next())
    }
    return acc
}

// Usage of the helper functions
func analyzeStock(catalog *ProductCatalog) {
    it := catalog.CreateIterator()
    totalValue := Reduce(it, 0.0, func(acc float64, p *Product) float64 {
        if p.InStock {
            return acc + p.Price
        }
        return acc
    })
    fmt.Printf("Total value in stock: Rp %.0f\n", totalValue)

    it.Reset()
    cheapest := First(catalog.InStockIterator(), func(p *Product) bool {
        return p.Price < 500000
    })
    if cheapest != nil {
        fmt.Printf("Cheapest under 500k: %s\n", cheapest.Name)
    }
}

Testing the Iterator Pattern #

func TestSliceIterator_TraversesAllElements(t *testing.T) {
    catalog := NewProductCatalog()
    catalog.Add(&Product{ID: "P1", Name: "Product 1", Price: 100})
    catalog.Add(&Product{ID: "P2", Name: "Product 2", Price: 200})
    catalog.Add(&Product{ID: "P3", Name: "Product 3", Price: 300})

    it := catalog.CreateIterator()
    var visited []string
    for it.HasNext() {
        p := it.Next()
        visited = append(visited, p.ID)
    }

    if len(visited) != 3 {
        t.Errorf("expected 3 elements, got %d", len(visited))
    }
    if visited[0] != "P1" || visited[1] != "P2" || visited[2] != "P3" {
        t.Errorf("wrong traversal order: %v", visited)
    }
}

func TestSliceIterator_Reset(t *testing.T) {
    catalog := NewProductCatalog()
    catalog.Add(&Product{ID: "P1", Name: "Product 1"})
    catalog.Add(&Product{ID: "P2", Name: "Product 2"})

    it := catalog.CreateIterator()
    _ = it.Next() // advance one step
    _ = it.Next() // advance again
    if it.HasNext() {
        t.Error("expected no more elements")
    }

    it.Reset()
    if !it.HasNext() {
        t.Error("after reset, should have elements again")
    }
    if p := it.Next(); p.ID != "P1" {
        t.Errorf("after reset, first element should be P1, got %s", p.ID)
    }
}

func TestFilteredIterator_OnlyReturnsMatching(t *testing.T) {
    catalog := NewProductCatalog()
    catalog.Add(&Product{ID: "P1", Name: "Laptop", InStock: true, Price: 10000000})
    catalog.Add(&Product{ID: "P2", Name: "Mouse", InStock: false, Price: 200000})
    catalog.Add(&Product{ID: "P3", Name: "Keyboard", InStock: true, Price: 1500000})

    it := catalog.InStockIterator()
    var inStock []string
    for it.HasNext() {
        inStock = append(inStock, it.Next().ID)
    }

    if len(inStock) != 2 {
        t.Errorf("expected 2 in-stock products, got %d", len(inStock))
    }
    if inStock[0] != "P1" || inStock[1] != "P3" {
        t.Errorf("wrong filtered elements: %v", inStock)
    }
}

func TestMultipleIterators_IndependentTraversal(t *testing.T) {
    catalog := NewProductCatalog()
    for i := 1; i <= 5; i++ {
        catalog.Add(&Product{ID: fmt.Sprintf("P%d", i), Name: fmt.Sprintf("Product %d", i)})
    }

    it1 := catalog.CreateIterator()
    it2 := catalog.CreateIterator()

    // Advance it1 two steps
    _ = it1.Next()
    p1 := it1.Next()

    // it2 is still at the start — unaffected by it1
    p2 := it2.Next()

    if p1.ID != "P2" {
        t.Errorf("it1 second element: expected P2, got %s", p1.ID)
    }
    if p2.ID != "P1" {
        t.Errorf("it2 first element: expected P1, got %s", p2.ID)
    }
}

func TestSortedIterator_CorrectOrder(t *testing.T) {
    catalog := NewProductCatalog()
    catalog.Add(&Product{ID: "P3", Price: 300})
    catalog.Add(&Product{ID: "P1", Price: 100})
    catalog.Add(&Product{ID: "P2", Price: 200})

    it := catalog.CreateSortedIterator(func(a, b *Product) bool {
        return a.Price < b.Price
    })

    var order []string
    for it.HasNext() {
        order = append(order, it.Next().ID)
    }

    if order[0] != "P1" || order[1] != "P2" || order[2] != "P3" {
        t.Errorf("wrong sort order: %v", order)
    }
}

When to Use and When Not to #

USE Iterator if:
  ✓ You need to traverse a collection with complex internal structure (tree, graph)
  ✓ You need multiple simultaneous traversals over the same collection
  ✓ You need lazy evaluation for large datasets (database cursors, file streams)
  ✓ You want to separate traversal logic from storage logic
  ✓ You need several different traversal modes (DFS, BFS, filtered, sorted)

AVOID Iterator if:
  ✗ The collection is a simple slice/array — for-range is enough and more idiomatic
  ✗ You only need one traversal, once — direct for-range is simpler
  ✗ The collection is very small — the interface overhead is not worth it
  ✗ Go 1.22+ is available and the collection already implements iter.Seq — use that

Iterators in the Go Standard Library

bufio.Scanner is an iterator — scanner.Scan() is equivalent to HasNext(), scanner.Text() is equivalent to Next(). database/sql.Rows is also an iterator — rows.Next() and rows.Scan(). filepath.Walk uses callback-based iteration. Understanding the Iterator Pattern helps you read and understand these standard APIs more intuitively.


Iterator Review Checklist #

DESIGN:
  □ The Iterator encapsulates traversal — the client does not know the collection's internal structure
  □ Each Iterator is an independent object — several can run simultaneously
  □ The collection provides factory methods (CreateIterator) to create iterators
  □ Iterators do not modify the collection during traversal

IMPLEMENTATION:
  □ HasNext() has no side effects — calling it multiple times is safe
  □ Next() is only called after HasNext() returns true
  □ Reset() returns the iterator to its initial position
  □ The FilteredIterator prefetches correctly for accurate HasNext()

LAZY EVALUATION:
  □ Database iterators do not load all rows at once
  □ Channel iterators use a context for cancellation
  □ Iterators close resources (rows.Close(), channel) when finished or cancelled

TESTING:
  □ Every element is visited exactly once
  □ Traversal order is verified (for sorted/DFS/BFS iterators)
  □ Multiple iterators run independently of each other
  □ Reset() correctly returns to the initial position
  □ The FilteredIterator only returns elements satisfying the predicate

Summary #

  • Iterator separates “how to traverse” from “how to store” — the client uses HasNext() and Next() without needing to know whether the collection is a slice, tree, database cursor, or generator.
  • Four approaches in Go: the classic interface (HasNext/Next), channel-based, functional (iter.Seq in Go 1.23+), and callback — pick according to context and Go version.
  • Multiple independent iterators — each iterator has its own traversal state; several can run simultaneously on the same collection without interfering.
  • A FilteredIterator is a Decorator on the Iterator — it wraps another iterator and only passes through elements satisfying the predicate; no need to modify the original collection.
  • Lazy evaluation for large datasets — database cursors and channel-based iterators load only one element at a time; no need to load the entire dataset into memory.
  • Helper functions like Collect, First, Count, and Reduce make client code more expressive without manual loops.
  • The standard library already uses Iterators: bufio.Scanner, database/sql.Rows, and filepath.Walk all implement the same concept.
  • For simple slices, for-range is enough — an Iterator is only needed when there is traversal complexity or a need for multiple simultaneous traversals.

← Previous: Memento   Next: Visitor →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact