Go 1.22 introduced iterating over functions with for ... range. I’m trying to make a generator that respects context.Context so that breaking early doesn’t leak a goroutine.
package main
import (
"context"
"fmt"
"time"
)
func numbers(ctx context.Context, n int) func(yield func(int) bool) {
return func(yield func(int) bool) {
t := time.NewTicker(50 * time.Millisecond)
defer t.Stop()
for i := 0; i < n; i++ {
select {
case <-ctx.Done():
// try to exit cleanly
return
case <-t.C:
if !yield(i) { // consumer broke early
return
}
}
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
count := 0
for i := range numbers(ctx, 1000) {
fmt.Println(i)
count++
if count == 5 {
break // stop after 5 values
}
}
// give some time to observe if anything is still running
time.Sleep(200 * time.Millisecond)
fmt.Println("done")
}
Breaking after 5 values should stop the generator immediately with no background work left (no leaked goroutines or pending timers).
What I’m unsure about:
- Is the pattern above enough to guarantee no leaks when the consumer breaks early?
- Should the generator also watch for the
yieldfunction being blocked and use a separate goroutine + select? - Is there a canonical way to combine
context.Contextwith “range over func” so early cancellation is safe, similar to how we handle channels?
Environment:
- Go 1.22/1.23
- Linux/macOS