Collect errors from goroutines nested in loops

Issue

I’m trying to collect errors from goroutines in loop, but dont’t understand how it must correctly work
https://go.dev/play/p/WrxE0vH6JSG

func init() {
    rand.Seed(1500929006430687579)
}

func goroutine(n int, wg *sync.WaitGroup, ch chan error) {
    defer wg.Done()
    defer fmt.Println("defer done")

    fmt.Println("num ", n)
    if n == 1 {
        ch <- fmt.Errorf("error")
    } else {
        ch <- nil
    }
}

func main() {
    var wg sync.WaitGroup
    var err error
    errs := make(chan error)
    platforms := 2
    types := 3
    for j := 0; j < platforms; j++ {
        wg.Add(1)
        for k := 0; k < types; k++ {
            wg.Add(1)
            n := rand.Intn(2)
            go goroutine(n, &wg, errs)
        }

        for k := 0; k < types; k++ {
            wg.Add(1)
            n := rand.Intn(2)
            go goroutine(n, &wg, errs)
        }
    }
    wg.Wait()
    err = <-errs
    fmt.Println(err)
}

how should I collect array of errors correctly and done all wait groups?

Solution

In Golang channels is similar to pipes in bash (|). But in contrast to bash pipes which are used to transport output of one command to input of another command, Go channels are used to transport some data between goroutines. You can read more about channels here.
Channels have capacity. When you don’t specify capacity for the channel go assumes that it has 0 capacity. Channels with zero capacity often called unbuffered channels while channels with non-zero capacity called buffered. When channel is full (number of elements in channel is equal to channel’s capacity) than all write operations on the channel (->errs) block execution flow until read operation (<-errs) will be presented.

In your particular example you have unbuffered channel (the channel with 0 capacity). Thus any write operation (->errs) on your channel will block the execution until some read operation would be provided, therefore all goroutines that you launched will be blocked despite the only one goroutine that would be able to proceed write operation when the flow of the main function moved forward to read operation (err = <-errs).

To solve your issue you could create one extra goroutine that would read from channel concurrently with goroutines that would write to channel. It will look like that:

func init() {
    rand.Seed(1500929006430687579)
}

func goroutine(n int, wg *sync.WaitGroup, ch chan error) {
    defer fmt.Println("defer done")
    defer wg.Done()

    fmt.Println("num ", n)
    if n == 1 {
        ch <- fmt.Errorf("error")
    }
}

func main() {
    var wg sync.WaitGroup
    errs := make(chan error)
    platforms := 2
    types := 3

    go func() {
        for e := range errs {
            fmt.Println(e)
        }
    }()

    for j := 0; j < platforms; j++ {
        for k := 0; k < types; k++ {
            wg.Add(1)
            n := rand.Intn(2)
            go goroutine(n, &wg, errs)
        }

        for k := 0; k < types; k++ {
            wg.Add(1)
            n := rand.Intn(2)
            go goroutine(n, &wg, errs)
        }
    }
    wg.Wait()
}

In addition you have several bugs and inaccuracies that I refactored in your code:

  1. You shouldn’t write nil in channel with errors. If you want errs chan to comprise only errors so write there only if your function executed with non-nil error.
  2. You had one extra wd.Add(1) as the beginning of j loop so there was disbalance between Add functions and Done function. 3.
  3. Furthemore, you add defer fmt.Println("defer done") after defer wg.Done() but defers constructions are executed in reversed order than they were specified so it would be more correct to put defer fmt.Println("defer done") before defer wg.Done() so that "defer done" would really signalize that all previous defers had been executed.

Answered By – cooleck

Answer Checked By – Marilyn (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.