🎫 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++"] -->|"<-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:
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.
In the standard library
time.Tick/time.NewTickergenerate a stream of timestamps.context.Context.Done()is a one-shot generator of a cancellation signal.bufio.Scanneris 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.
Related patterns
Process a stream of data through a series of stages connected by channels, where each stage is a goroutine.
BEHAVIORAL IteratorProvide a way to access the elements of a collection sequentially without exposing its underlying representation.
CONCURRENCY Or-done ChannelWrap a value stream so that ranging over it also stops cleanly the moment a cancellation signal fires.
Check your understanding
Score: 0 / 31. 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.