Skip to content
Corentin GS

Generator Pattern in Go: Lazy Sequences with Channels

№09 · · ·Updated ·792 words ·4 min read
Generator Pattern in Go: Lazy Sequences with Channels
In this piece
TL;DR

You need a sequence of values produced lazily, often infinite or async. A function returns a receive-only channel; a goroutine inside sends values and closes the channel when done. Use it for test data, log streams, iterators, and anywhere pulling values one at a time beats building a whole slice up front. Since Go 1.23, prefer iter.Seq for purely synchronous sequences — see the comparison below.

Introduction

The Generator pattern in Go is a powerful concurrency pattern used to create functions that produce a sequence of values. It leverages Go’s goroutines and channels to generate data asynchronously, providing an elegant way to work with streams of data or implement iterators.

When to Use

  • Creating sequences of numbers or data
  • Implementing iterators
  • Processing streams of data
  • Generating test data
  • Simulating real-time data sources

Why to Use

  • Lazy Evaluation: Values are generated on-demand, saving memory
  • Encapsulation: Hides the complexity of data generation
  • Concurrency: Allows for asynchronous data production
  • Flexibility: Can generate infinite or finite sequences
  • Composability: Generators can be chained or combined easily

How it Works

  1. A function creates and returns a channel
  2. The function starts a goroutine that sends values through the channel
  3. The caller receives values from the channel, typically using a for-range loop

Simple Example

func evenGenerator(max int) <-chan int {
    out := make(chan int)
    go func() {
        for i := 0; i <= max; i += 2 {
            out <- i
        }
        close(out)
    }()
    return out
}

func main() {
    for num := range evenGenerator(10) {
        fmt.Println(num)
    }
}

This example creates a generator that produces even numbers up to a specified maximum. The evenGenerator function returns a receive-only channel (<-chan int). It starts a goroutine that sends even numbers through the channel and closes it when done.

Real-World Example: Log Line Generator

Let’s consider a scenario where we need to generate sample log lines for testing a log analysis system.

type LogEntry struct {
    Timestamp time.Time
    Level     string
    Message   string
}

func logGenerator(count int) <-chan LogEntry {
    out := make(chan LogEntry)
    go func() {
        levels := []string{"INFO", "WARNING", "ERROR"}
        messages := []string{
            "User logged in",
            "Failed login attempt",
            "Database connection lost",
            "API request received",
            "Cache miss",
        }
        for i := 0; i < count; i++ {
            out <- LogEntry{
                Timestamp: time.Now().Add(time.Duration(i) * time.Second),
                Level:     levels[rand.Intn(len(levels))],
                Message:   messages[rand.Intn(len(messages))],
            }
            time.Sleep(100 * time.Millisecond) // Simulate delay between log entries
        }
        close(out)
    }()
    return out
}

func main() {
    for entry := range logGenerator(5) {
        fmt.Printf("[%s] %s: %s\n", entry.Timestamp.Format(time.RFC3339), entry.Level, entry.Message)
    }
}

This generator creates a stream of log entries, simulating real-world log generation. It’s useful for testing log processing systems, allowing developers to generate a controlled stream of diverse log entries.

Generators vs. iter.Seq (Go 1.23+)

Since Go 1.23, the standard library offers another way to build lazy sequences: range-over-func iterators (iter.Seq, iter.Seq2). An iterator is a function that yields values one at a time — no goroutine, no channel:

func evens(max int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := 0; i <= max; i += 2 {
            if !yield(i) {
                return // consumer stopped early
            }
        }
    }
}

func main() {
    for n := range evens(10) {
        fmt.Println(n)
    }
}

The two approaches overlap, but they are not interchangeable.

Reach for iter.Seq when:

  • The consumer drives the pace and everything is synchronous
  • You want to compose with the standard library (slices.Collect, maps.Keys, slices.Sorted)
  • You want zero goroutine management — an iterator cannot leak

Keep the channel generator when:

  • Values are produced concurrently or asynchronously — I/O, timers, network events
  • You need a buffer for backpressure between producer and consumer
  • The sequence feeds a concurrent pipeline. A generator channel plugs directly into a worker pool or a producer-consumer setup; an iter.Seq does not.

A useful mental model: iter.Seq pulls, a channel generator pushes. If nothing in your program needs to happen at the same time as something else, pulling is simpler.

Best Practices and Pitfalls

Best Practices:

  1. Always close the channel when generation is complete
  2. Use receive-only channels (<-chan) as return types
  3. Consider using context for cancellation in long-running generators
  4. Implement error handling for robust generators

Pitfalls:

  1. Forgetting to close channels, leading to goroutine leaks
  2. Creating infinite generators without proper termination conditions
  3. Blocking indefinitely on channel operations without timeout mechanisms
  4. Overusing generators for simple, finite sequences where slices might suffice

Summary

The Generator pattern in Go provides a powerful way to create sequences or streams of data asynchronously. By leveraging goroutines and channels, it offers lazy evaluation, encapsulation of complex logic, and seamless integration with Go’s concurrency model. Whether you’re working with infinite sequences, simulating data sources, or implementing iterators, the Generator pattern offers a flexible and efficient solution.

Testing Generators

Generators are easy to test when you treat them as contracts: send input, receive output. The goroutine is an implementation detail. I wrote about why that matters in TDD Isn’t About Bugs — It’s Your Permission to Refactor.


Ready for bigger examples? Advanced Generator Patterns composes generators into test-data pipelines and streaming stages.

If you want to experiment with the code examples, you can find them on my GitHub repository.

Next in Go Patterns

Mastering the Worker Pool Pattern in Go

Part 4 continues the series.