{} The Go Reference

Concurrency pattern · Beginner

Generator

Produce a stream of values from a goroutine over a channel, lazily and on demand.

Concurrency Beginner ⏱ 3 min read Complete

🎫 Analogy

A ticket dispenser produces numbers on demand: you pull one, then the next, then the next. It doesn’t pre-print a million tickets — it makes each as you ask. A generator is that dispenser, handing values to a consumer one at a time over a channel.

The problem

You want a sequence — maybe infinite — that a consumer reads lazily, without building the whole thing in memory first. In Go, the natural shape is a function that returns a channel and runs a goroutine that feeds it. The consumer just ranges the channel.

Structure

graph LR
G["generator goroutine<br/>for i := 1; ; i++"] -->|"&lt;-chan int"| C["consumer<br/>range / receive"]
D["done chan"] -.cancels.-> G

Idiomatic Go

count produces integers forever; the consumer takes only what it needs and cancels the rest via done. Edit and Run:

generator.go — editable & runnable
package main

import "fmt"

// count returns a stream of integers 1, 2, 3, … on a channel.
func count(done <-chan struct{}) <-chan int {
out := make(chan int)
go func() {
	defer close(out)
	for i := 1; ; i++ {
		select {
		case <-done: // cancellation: stop and clean up
			return
		case out <- i:
		}
	}
}()
return out
}

func main() {
done := make(chan struct{})
defer close(done) // stops the generator when main returns

nums := count(done)
for i := 0; i < 5; i++ {
	fmt.Println(<-nums) // pull five values: 1 2 3 4 5
}
}

🐹 The source of every pipeline

A generator is just the first stage of a Pipeline: it produces, downstream stages transform. Two rules carry over — the producer closes the channel, and an infinite generator must select on a cancellation signal (a done channel or ctx.Done()) so it can’t block forever once the consumer walks away. For synchronous sequences with no concurrency, Go 1.23’s range-over-func (iter.Seq) is the simpler tool — see Iterator.

Channel generator vs range-over-func

Go 1.23 gave generators a synchronous sibling. Choose by whether production is concurrent:

Channel generatorrange-over-func (iter.Seq)
Runs inits own goroutinethe caller’s goroutine (synchronous)
Costa goroutine + a channela function call per value
Cancellationneeds done/ctx or it leaksbreak just stops the loop
Best forconcurrent/blocking producers, pipeline sourcespure lazy sequences, no concurrency

If there’s no concurrency, prefer iter.Seq — it can’t leak a goroutine. Reach for the channel form when the producer genuinely runs alongside the consumer (a network reader, a pipeline stage). See Iterator for the range-over-func form.

In the standard library

  • time.Tick / time.NewTicker generate a stream of timestamps.
  • context.Context.Done() is a one-shot generator of a cancellation signal.
  • bufio.Scanner is the synchronous cousin — a pull-based token generator.

Pitfalls

⚠️ An infinite generator with no exit leaks

If count had no case <-done, then the moment the consumer stops receiving, the generator’s goroutine blocks forever on out <- i — a leak that survives until the process dies. Always give an unbounded generator a way out.

When to use it — and when not

✅ Reach for it when

  • You want a lazy or infinite sequence the consumer can pull from at its own pace.
  • You're building the source stage of a pipeline.
  • You want to decouple producing values from consuming them.

⛔ Think twice when

  • It's a small finite slice — just `range` it.
  • No concurrency is needed — Go 1.23 range-over-func (`iter.Seq`) is simpler for synchronous sequences.

Check your understanding

Score: 0 / 5

1. What does a Go generator return?

The idiom is `func gen(...) <-chan T` — it starts a goroutine that sends values on a channel it returns, so the caller pulls them lazily.

2. Who closes the generator's channel?

The producer owns the channel and closes it when finished, so the consumer's `range` ends cleanly.

3. How do you stop an infinite generator without leaking its goroutine?

An infinite generator must watch a cancellation signal (a done channel or ctx.Done()); otherwise it blocks forever on a send once the consumer stops reading.

4. When should you use a channel generator vs Go 1.23's range-over-func (iter.Seq)?

A channel generator runs the producer in its own goroutine — right when production is concurrent or blocks on I/O, but it costs a goroutine and needs cancellation. iter.Seq (range-over-func) is a synchronous pull with no goroutine, no channel, no leak risk — better for pure in-process sequences.

5. With an unbuffered channel, how fast does the generator produce?

An unbuffered channel couples producer to consumer: the generator blocks on send until the consumer receives, so it produces lazily and memory stays bounded — the same back-pressure that makes [pipelines](/patterns/pipeline/) safe.

Comments

Sign in with GitHub to join the discussion.