Do the 'done' channel and default case lead to goroutine leak

Viewed 66

There are two similar cases I would like to compare with you - the only difference is the way of handling values generation

  • the first case: values generation in one of case of select
package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    generateValues := func(done <-chan interface{}) <-chan int {
        values := make(chan int)
        go func() {
            defer fmt.Println("All values generated")
            defer close(values)
            for {
                select {
                case <-done:
                    fmt.Println("DONE")
                    return
                case values <- rand.Int():
                    fmt.Println("Generated")
                }

            }

        }()
        return values
    }

    done := make(chan interface{})
    values := generateValues(done)

    for i := 0; i < 3; i++ {
        fmt.Printf("Received value: %v\n", <-values)
    }
    fmt.Println("Closing the channel")
    close(done)
    time.Sleep(5 * time.Second)
}

Go playground: https://go.dev/play/p/edlOSqdZ9ys

  • the second case: value generation in default case
package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    generateValues := func(done <-chan interface{}) <-chan int {
        values := make(chan int)
        go func() {
            defer fmt.Println("All values generated")
            defer close(values)
            for {
                select {
                case <-done:
                    fmt.Println("DONE")
                    return
                default:
                    values <- rand.Int()
                    fmt.Println("Generated")
                }

            }

        }()
        return values
    }

    done := make(chan interface{})
    values := generateValues(done)

    for i := 0; i < 3; i++ {
        fmt.Printf("Received value: %v\n", <-values)
    }
    fmt.Println("Closing the channel")
    close(done)
    time.Sleep(5 * time.Second)
}

Go playground: https://go.dev/play/p/edlOSqdZ9ys

As you can see the second case seems leading to the situation that the 'Done' is not printed and calls related to 'defer' are not invoked. I believe we have here goroutine leaks, but cannot clearly explain it. I expected the same behaviour like in the first case.

Could someone please help in understanding the difference between them?

1 Answers

In the second case, the generating goroutine is not likely to receive the done message. Since the default case is always enabled, after the main goroutine receives the last value, the generating goroutine makes another round, falls into the default case, and blocks waiting to write to the values channel. While it is waiting there, the main goroutine closes the done channel and terminates.

This doesn't mean there doesn't exist an execution path where the generating goroutine doesn't receive the done channel. For this to happen, immediately after sending the last value, the generating goroutine must be preempted by the main goroutine that runs until it closes the done channel. Then if the generating goroutine is scheduled, it can receive the done signal. However, this sequence of events is highly unlikely.

Related