📚 Go Design Patterns 🎯Behavioral Pattern

What is Iterator Pattern?

The Iterator pattern provides a way to access elements of a collection sequentially without exposing the underlying representation. It’s like having a remote control for your TV - you don’t need to know how the channels are stored internally, you just press “next” to move through them.

In Go, the Iterator pattern is especially relevant with the introduction of range-over-func in Go 1.23, which allows you to create custom iterators that work seamlessly with the range keyword. This makes your collections feel native to the language.

Let’s start with a scenario: Custom Book Library

Imagine you’re building a digital library system that stores books in different data structures - some in arrays, some in trees, and some in custom structures. You want to provide a consistent way to iterate through books regardless of how they’re stored internally.

Without Iterator Pattern

Here’s how you might handle this without the Iterator pattern:

type Book struct {
    Title  string
    Author string
    ISBN   string
}

type Library struct {
    books []*Book
}

func (l *Library) GetAllBooks() []*Book {
    // Exposes internal structure
    return l.books
}

func main() {
    library := &Library{
        books: []*Book{
            {Title: "The Go Programming Language", Author: "Donovan & Kernighan", ISBN: "978-0134190440"},
            {Title: "Clean Code", Author: "Robert Martin", ISBN: "978-0132350884"},
        },
    }

    // Client code depends on internal array structure
    for _, book := range library.GetAllBooks() {
        fmt.Printf("%s by %s\n", book.Title, book.Author)
    }
}

This approach has problems: it exposes the internal data structure, makes it hard to change implementation later, doesn’t support filtering or transformation during iteration, and forces the entire collection into memory.

With Iterator Pattern (Traditional Approach)

Let’s implement a traditional iterator first:

package main

import "fmt"

type Book struct {
    Title  string
    Author string
    ISBN   string
}

// Iterator interface
type Iterator interface {
    HasNext() bool
    Next() *Book
    Reset()
}

// Collection interface
type Collection interface {
    CreateIterator() Iterator
}

// Concrete iterator
type BookIterator struct {
    books   []*Book
    current int
}

func (i *BookIterator) HasNext() bool {
    return i.current < len(i.books)
}

func (i *BookIterator) Next() *Book {
    if i.HasNext() {
        book := i.books[i.current]
        i.current++
        return book
    }
    return nil
}

func (i *BookIterator) Reset() {
    i.current = 0
}

// Concrete collection
type Library struct {
    books []*Book
}

func (l *Library) CreateIterator() Iterator {
    return &BookIterator{
        books:   l.books,
        current: 0,
    }
}

func (l *Library) AddBook(book *Book) {
    l.books = append(l.books, book)
}

func main() {
    library := &Library{}
    library.AddBook(&Book{Title: "The Go Programming Language", Author: "Donovan & Kernighan", ISBN: "978-0134190440"})
    library.AddBook(&Book{Title: "Clean Code", Author: "Robert Martin", ISBN: "978-0132350884"})
    library.AddBook(&Book{Title: "Design Patterns", Author: "Gang of Four", ISBN: "978-0201633610"})

    // Using the iterator
    iterator := library.CreateIterator()
    for iterator.HasNext() {
        book := iterator.Next()
        fmt.Printf("%s by %s\n", book.Title, book.Author)
    }

    // Can reset and iterate again
    iterator.Reset()
    fmt.Println("\nIterating again:")
    for iterator.HasNext() {
        book := iterator.Next()
        fmt.Printf("ISBN: %s - %s\n", book.ISBN, book.Title)
    }
}

Go 1.23 Iterator Pattern with Range-over-Func

Go 1.23 introduced a more idiomatic way to implement iterators using the range keyword. Here’s the modern approach:

package main

import (
    "fmt"
    "iter"
)

type Book struct {
    Title  string
    Author string
    ISBN   string
    Year   int
}

type Library struct {
    books []*Book
}

func (l *Library) AddBook(book *Book) {
    l.books = append(l.books, book)
}

// Iterator that yields books
func (l *Library) All() iter.Seq[*Book] {
    return func(yield func(*Book) bool) {
        for _, book := range l.books {
            if !yield(book) {
                return
            }
        }
    }
}

// Iterator with index
func (l *Library) AllWithIndex() iter.Seq2[int, *Book] {
    return func(yield func(int, *Book) bool) {
        for i, book := range l.books {
            if !yield(i, book) {
                return
            }
        }
    }
}

// Filtered iterator - books by author
func (l *Library) ByAuthor(author string) iter.Seq[*Book] {
    return func(yield func(*Book) bool) {
        for _, book := range l.books {
            if book.Author == author {
                if !yield(book) {
                    return
                }
            }
        }
    }
}

// Filtered iterator - books after year
func (l *Library) AfterYear(year int) iter.Seq[*Book] {
    return func(yield func(*Book) bool) {
        for _, book := range l.books {
            if book.Year > year {
                if !yield(book) {
                    return
                }
            }
        }
    }
}

// Reverse iterator
func (l *Library) Reverse() iter.Seq[*Book] {
    return func(yield func(*Book) bool) {
        for i := len(l.books) - 1; i >= 0; i-- {
            if !yield(l.books[i]) {
                return
            }
        }
    }
}

func main() {
    library := &Library{}
    library.AddBook(&Book{Title: "The Go Programming Language", Author: "Donovan & Kernighan", ISBN: "978-0134190440", Year: 2015})
    library.AddBook(&Book{Title: "Clean Code", Author: "Robert Martin", ISBN: "978-0132350884", Year: 2008})
    library.AddBook(&Book{Title: "Design Patterns", Author: "Gang of Four", ISBN: "978-0201633610", Year: 1994})
    library.AddBook(&Book{Title: "The Clean Coder", Author: "Robert Martin", ISBN: "978-0137081073", Year: 2011})
    library.AddBook(&Book{Title: "Refactoring", Author: "Martin Fowler", ISBN: "978-0134757599", Year: 2018})

    // Iterate through all books using range
    fmt.Println("All Books:")
    for book := range library.All() {
        fmt.Printf("  %s by %s (%d)\n", book.Title, book.Author, book.Year)
    }

    // Iterate with index
    fmt.Println("\nBooks with Index:")
    for i, book := range library.AllWithIndex() {
        fmt.Printf("  %d. %s\n", i+1, book.Title)
    }

    // Filter by author
    fmt.Println("\nBooks by Robert Martin:")
    for book := range library.ByAuthor("Robert Martin") {
        fmt.Printf("  %s (%d)\n", book.Title, book.Year)
    }

    // Filter by year
    fmt.Println("\nBooks published after 2010:")
    for book := range library.AfterYear(2010) {
        fmt.Printf("  %s (%d)\n", book.Title, book.Year)
    }

    // Reverse iteration
    fmt.Println("\nBooks in Reverse Order:")
    for book := range library.Reverse() {
        fmt.Printf("  %s\n", book.Title)
    }
}

Output:

All Books:
  The Go Programming Language by Donovan & Kernighan (2015)
  Clean Code by Robert Martin (2008)
  Design Patterns by Gang of Four (1994)
  The Clean Coder by Robert Martin (2011)
  Refactoring by Martin Fowler (2018)

Books with Index:
  1. The Go Programming Language
  2. Clean Code
  3. Design Patterns
  4. The Clean Coder
  5. Refactoring

Books by Robert Martin:
  Clean Code (2008)
  The Clean Coder (2011)

Books published after 2010:
  The Go Programming Language (2015)
  The Clean Coder (2011)
  Refactoring (2018)

Books in Reverse Order:
  Refactoring
  The Clean Coder
  Design Patterns
  Clean Code
  The Go Programming Language

Advanced Example: Tree Iterator

Here’s a more complex example showing how to iterate through a binary tree:

package main

import (
    "fmt"
    "iter"
)

type TreeNode struct {
    Value int
    Left  *TreeNode
    Right *TreeNode
}

type BinaryTree struct {
    Root *TreeNode
}

func (t *BinaryTree) Insert(value int) {
    if t.Root == nil {
        t.Root = &TreeNode{Value: value}
        return
    }
    t.Root.insert(value)
}

func (n *TreeNode) insert(value int) {
    if value < n.Value {
        if n.Left == nil {
            n.Left = &TreeNode{Value: value}
        } else {
            n.Left.insert(value)
        }
    } else {
        if n.Right == nil {
            n.Right = &TreeNode{Value: value}
        } else {
            n.Right.insert(value)
        }
    }
}

// In-order traversal (Left -> Root -> Right)
func (t *BinaryTree) InOrder() iter.Seq[int] {
    return func(yield func(int) bool) {
        var traverse func(*TreeNode) bool
        traverse = func(node *TreeNode) bool {
            if node == nil {
                return true
            }
            if !traverse(node.Left) {
                return false
            }
            if !yield(node.Value) {
                return false
            }
            return traverse(node.Right)
        }
        traverse(t.Root)
    }
}

// Pre-order traversal (Root -> Left -> Right)
func (t *BinaryTree) PreOrder() iter.Seq[int] {
    return func(yield func(int) bool) {
        var traverse func(*TreeNode) bool
        traverse = func(node *TreeNode) bool {
            if node == nil {
                return true
            }
            if !yield(node.Value) {
                return false
            }
            if !traverse(node.Left) {
                return false
            }
            return traverse(node.Right)
        }
        traverse(t.Root)
    }
}

// Post-order traversal (Left -> Right -> Root)
func (t *BinaryTree) PostOrder() iter.Seq[int] {
    return func(yield func(int) bool) {
        var traverse func(*TreeNode) bool
        traverse = func(node *TreeNode) bool {
            if node == nil {
                return true
            }
            if !traverse(node.Left) {
                return false
            }
            if !traverse(node.Right) {
                return false
            }
            return yield(node.Value)
        }
        traverse(t.Root)
    }
}

// Level-order traversal (Breadth-first)
func (t *BinaryTree) LevelOrder() iter.Seq[int] {
    return func(yield func(int) bool) {
        if t.Root == nil {
            return
        }

        queue := []*TreeNode{t.Root}
        for len(queue) > 0 {
            node := queue[0]
            queue = queue[1:]

            if !yield(node.Value) {
                return
            }

            if node.Left != nil {
                queue = append(queue, node.Left)
            }
            if node.Right != nil {
                queue = append(queue, node.Right)
            }
        }
    }
}

func main() {
    tree := &BinaryTree{}
    values := []int{50, 30, 70, 20, 40, 60, 80}

    for _, v := range values {
        tree.Insert(v)
    }

    fmt.Println("In-Order (sorted):")
    for value := range tree.InOrder() {
        fmt.Printf("%d ", value)
    }
    fmt.Println()

    fmt.Println("\nPre-Order:")
    for value := range tree.PreOrder() {
        fmt.Printf("%d ", value)
    }
    fmt.Println()

    fmt.Println("\nPost-Order:")
    for value := range tree.PostOrder() {
        fmt.Printf("%d ", value)
    }
    fmt.Println()

    fmt.Println("\nLevel-Order (breadth-first):")
    for value := range tree.LevelOrder() {
        fmt.Printf("%d ", value)
    }
    fmt.Println()
}

Output:

In-Order (sorted):
20 30 40 50 60 70 80

Pre-Order:
50 30 20 40 70 60 80

Post-Order:
20 40 30 60 80 70 50

Level-Order (breadth-first):
50 30 70 20 40 60 80

Real-World Example: Database Result Iterator

Here’s a practical example showing how to implement an iterator for database results:

package main

import (
    "fmt"
    "iter"
    "time"
)

type User struct {
    ID        int
    Username  string
    Email     string
    CreatedAt time.Time
}

// Simulated database connection
type Database struct {
    users []*User
}

func NewDatabase() *Database {
    return &Database{
        users: []*User{
            {ID: 1, Username: "alice", Email: "[email protected]", CreatedAt: time.Now().AddDate(-1, 0, 0)},
            {ID: 2, Username: "bob", Email: "[email protected]", CreatedAt: time.Now().AddDate(0, -6, 0)},
            {ID: 3, Username: "charlie", Email: "[email protected]", CreatedAt: time.Now().AddDate(0, -3, 0)},
            {ID: 4, Username: "diana", Email: "[email protected]", CreatedAt: time.Now().AddDate(0, -1, 0)},
        },
    }
}

// Paginated iterator - fetches users in batches
func (db *Database) Users(batchSize int) iter.Seq[*User] {
    return func(yield func(*User) bool) {
        for i := 0; i < len(db.users); i += batchSize {
            end := i + batchSize
            if end > len(db.users) {
                end = len(db.users)
            }

            // Simulate database query with batch
            batch := db.users[i:end]
            fmt.Printf("Fetching batch: %d-%d\n", i, end)

            for _, user := range batch {
                if !yield(user) {
                    return
                }
            }
        }
    }
}

// Iterator with transformation
func (db *Database) UserEmails() iter.Seq[string] {
    return func(yield func(string) bool) {
        for _, user := range db.users {
            if !yield(user.Email) {
                return
            }
        }
    }
}

// Filtered iterator with lazy evaluation
func (db *Database) UsersCreatedAfter(date time.Time) iter.Seq[*User] {
    return func(yield func(*User) bool) {
        for _, user := range db.users {
            if user.CreatedAt.After(date) {
                if !yield(user) {
                    return
                }
            }
        }
    }
}

func main() {
    db := NewDatabase()

    // Paginated iteration
    fmt.Println("Paginated Users (batch size: 2):")
    for user := range db.Users(2) {
        fmt.Printf("  User: %s (ID: %d)\n", user.Username, user.ID)
    }

    // Email extraction
    fmt.Println("\nAll User Emails:")
    for email := range db.UserEmails() {
        fmt.Printf("  %s\n", email)
    }

    // Filtered iteration
    fmt.Println("\nUsers created in last 4 months:")
    fourMonthsAgo := time.Now().AddDate(0, -4, 0)
    for user := range db.UsersCreatedAfter(fourMonthsAgo) {
        fmt.Printf("  %s (created: %s)\n", user.Username, user.CreatedAt.Format("2006-01-02"))
    }
}

Why use the Iterator pattern?

From my experience building Go applications, here are the key benefits:

  1. Encapsulation: Hide internal data structure details. Your library can use arrays today and switch to linked lists tomorrow without breaking client code.

  2. Lazy Evaluation: With Go 1.23 iterators, you can generate values on-demand, perfect for large datasets or infinite sequences.

  3. Multiple Traversals: Support different ways to iterate through the same collection (forward, backward, filtered, transformed).

  4. Memory Efficiency: Process large datasets without loading everything into memory. Great for database results, file processing, or stream processing.

  5. Composability: Combine iterators to create complex iteration logic. Chain filters, transformations, and limits.

  6. Standard Interface: With iter.Seq and iter.Seq2, your iterators work seamlessly with Go’s range keyword, making them feel native.

Real-world use cases:

  1. Database Query Results: Paginate through large result sets without loading everything into memory.
  2. File Processing: Read and process large files line by line or chunk by chunk.
  3. API Pagination: Iterate through paginated API responses transparently.
  4. Tree/Graph Traversal: Implement different traversal strategies (DFS, BFS, in-order, pre-order).
  5. Stream Processing: Process infinite or very large data streams.
  6. Cache Iteration: Iterate through cache entries with different eviction policies.

Caveats

While the Iterator pattern is powerful, be aware of these considerations:

  1. Overkill for Simple Cases: If you just have a simple slice and need basic iteration, exposing the slice directly might be simpler.

  2. State Management: Traditional iterators maintain state. Be careful with concurrent access or multiple simultaneous iterations.

  3. Go 1.23 Requirement: The modern iter.Seq approach requires Go 1.23+. For older versions, stick with traditional iterator interfaces.

  4. Performance: Iterators add a layer of indirection. For performance-critical code, benchmark before and after.

  5. Complexity: Don’t force the pattern. If your collection is simple and won’t change, a straightforward slice might be better.

Best Practices

  1. Use iter.Seq for Go 1.23+: It’s idiomatic and works with range.

  2. Support Early Termination: Always check the yield return value and stop if it returns false.

  3. Provide Multiple Iterators: Offer different iteration strategies (filtered, reversed, indexed) as separate methods.

  4. Document Behavior: Make it clear whether your iterator is lazy, whether it can be reset, and if it’s safe for concurrent use.

  5. Consider Generics: Use generics to make your iterators type-safe and reusable across different types.

Thank you

Thank you for reading! The Iterator pattern is one of the most practical design patterns, especially with Go 1.23’s new iteration features. It’s a pattern I use regularly in production code for database access, file processing, and API pagination. If you have questions or want to share your experiences with iterators in Go, please drop an email at [email protected]. Happy coding!