Home > Net >  Idiomatic goroutine concurrency and error handling
Idiomatic goroutine concurrency and error handling

Time:06-17

In the below code block I am trying to run several routines and get results (Whether success or error) for all of them.

package main
    
    import (
        "fmt"
        "sync"
    )
    
    func processBatch(num int, errChan chan<- error, resultChan chan<- int, wg *sync.WaitGroup) {
        defer wg.Done()
    
        if num == 3 {
            resultChan <- 0
            errChan <- fmt.Errorf("goroutine %d's error returned", num)
        } else {
            square := num * num
    
            resultChan <- square
    
            errChan <- nil
        }
    
    }
    
    func main() {
        var wg sync.WaitGroup
    
        batches := [5]int{1, 2, 3, 4, 5}
    
        resultChan := make(chan int)
        errChan := make(chan error)
    
        for i := range batches {
            wg.Add(1)
            go processBatch(batches[i], errChan, resultChan, &wg)
        }
    
        var results [5]int
        var err [5]error
        for i := range batches {
            results[i] = <-resultChan
            err[i] = <-errChan
        }
        wg.Wait()
        close(resultChan)
        close(errChan)
        fmt.Println(results)
        fmt.Println(err)
    }

Playground: https://go.dev/play/p/zA-Py9gDjce This code works and I get the result that I want i.e:

[25 1 4 0 16]
[<nil> <nil> <nil> goroutine 3's error returned <nil>]

I was wondering if there is a more idiomatic way to achieve this. I went through the errgroup package: https://pkg.go.dev/golang.org/x/sync/errgroup but wasn't able to find something that may help me here. Any suggestions are welcome.

CodePudding user response:

Waitgroup is redundant in this code. Execution is perfectly synced with the loop that is waiting for the channel's result. Code is not moved forward until all functions finish their work and posted results are read from the channels. Waitgroup is only necessary if your function needs to do any work AFTER results are posted to channels.

I also prefer a slightly different implementation. In a posted implementation, we are not sending both results and errors into the channels every time when the function is executed. Instead, we can send only the result for successful execution and send only an error when the code fails.

The advantage is simplified results/errors processing. We are getting slices of results and errors without nils.

In this example, the function returns a number, and we send its default value of 0 in case of error. It could be complicated to filter out not successful execution results from successful if zero could be legit function execution result.

Same with errors. To check if we have any errors, we can use simple code like if len(errs) != 0.

package main

import (
    "fmt"
)

func processBatch(num int, errChan chan<- error, resultChan chan<- int) {
    if num == 3 {
        // no need to send result when it is meanenless
        // resultChan <- 0
        errChan <- fmt.Errorf("goroutine %d's error returned", num)
    } else {
        square := num * num

        resultChan <- square

        // no need to send errror when it is nil
        // errChan <- nil
    }

}

func main() {
    batches := [5]int{1, 2, 3, 4, 5}

    resultChan := make(chan int)
    errChan := make(chan error)

    for i := range batches {
        go processBatch(batches[i], errChan, resultChan)
    }

    // use slices instead of arrays because legth varry now
    var results []int
    var errs []error

    // every time function executes it sends singe piece of data to one of two channels
    for range batches {
        select {
        case res := <-resultChan:
            results = append(results, res)
        case err := <-errChan:
            errs = append(errs, err)
        }
    }

    close(resultChan)
    close(errChan)

    fmt.Println(results)
    fmt.Println(errs)
}

https://go.dev/play/p/SYmfl8iGxgD

[25 1 16 4]
[goroutine 3's error returned]

If you can use external packages, we can get benefits from some multierr package. For example, github.com/hashicorp/go-multierror.

  • Related