Alexander Leyva Caro
Alexander Leyva Caro

Reputation: 1253

What happens when you break the for statement with a range channel

I'm following this code to get a lazy range of numbers with channels

// iterator
func iterator(n int, c chan int) {
    for i := 0; i < n; i++ {
        c <- i
    }
    close(c)
    fmt.Println("iterator End")
}

c := make(chan int)
go iterator(5, c)
for i := range c {
    fmt.Println(i)
}

This will print as expected

0
1
2
3
4
fmt.Println("iterator End")

But what happened when I break the for loop like this

c := make(chan int)
go getNumbers(5, c)
for i := range c {
    if i == 2 {
        break
    }
    fmt.Println(i)
}

It seems the goroutine is blocked because never prints iterator End (I also try by sleeping the main thread). I'm wondering how to handle this scenario? Did I need to use select to resolve this? There is any safe way to check if the range was break and stop the for-loop in the iterator?

Upvotes: 1

Views: 977

Answers (1)

colm.anseo
colm.anseo

Reputation: 22117

If a goroutine writes to an unbuffered channel and no other goroutine is reading from the channel - then the writes will block forever. This will cause a goroutine leak. This is what you are experiencing.

If you have a "producer" goroutine which writes to a channel, you need a way to signal it to stop. Closing the channel is not the critical part here - as channels are garbage collected when they go out of scope. A blocked goroutine (that will never unblock) is considered a leak as it will never be reclaimed, so you really need the goroutine to end.

You can signal an intent to quit in many ways - the two most popular being:

Signal: done channel

func iterator(n int, c chan int, done <-chan struct{}) {
    for i := 0; i < n; i++ {
        select {
        case c <- i:
        case <-done:
            break
        }
    }
    close(c)
    fmt.Println("iterator End")
}

reader goroutine:

c := make(chan int)
done := make(chan struct{})
go iterator(5, c, done)
for i := range c {
    if i == 2 {
        break
    }
    fmt.Println(i)
}
close(done) // signal writer goroutine to quit

Signal: context.Context

func iterator(ctx context.Context, n int, c chan int) {
        defer close(c)
        defer fmt.Println("iterator End")

        for i := 0; i < n; i++ {
                select {
                case c <- i:
                case <-ctx.Done():
                        fmt.Println("canceled. Reason:", ctx.Err())
                        return
                }
        }
}

read goroutine:

func run(ctx context.Context) {
        ctx, cancel := context.WithCancel(ctx)
        defer cancel()  // call this regardless - avoid context leaks - but signals producer your intent to stop
        c := make(chan int)
        go iterator(ctx, 5, c)
        for i := range c {
                if i == 2 {
                        break
                }
                fmt.Println(i)
        }
}

https://play.golang.org/p/4-fDyCurB7t

Upvotes: 4

Related Questions