3

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 yield function being blocked and use a separate goroutine + select?
  • Is there a canonical way to combine context.Context with “range over func” so early cancellation is safe, similar to how we handle channels?

Environment:

  • Go 1.22/1.23
  • Linux/macOS
1
  • There are no leaks here. Pattern is safe. There is nothing you can do if yield blocks but check context timeout again when ticker ticks so you do not call yield again if both cases of the select are enabled Commented Sep 5 at 4:20

1 Answer 1

5
+50

Your code is fine. The defer in the iterator function runs when the function returns, i.e. when the loop ends.

The main thing to keep in mind when working with iterators is that you must not continue the loop if yield returns false. If you do, that would result in a panic:

panic: runtime error: range function continued iteration after function for loop body returned false

Notably, using break in the outer for-range causes the yield function to return false.

As mentioned in the comments, it may happen that <-ctx.Done() and <-t.C are ready at the same time. The select then would randomly run one of the ready cases, meaning that it could choose case <-t.C even if the context is done. Checking <-ctx.Done() again within the ticker case is a solution.

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.