Search code examples
goerror-handlingconcurrencychannelgoroutine

How to return the error from the gouroutine inside a loop early?


I have a goroutine inside a loop and the way I am handling the error is that I add it to a channel and after all the goroutines are finished, I check if there was an error and I return accordingly.

The issue with this is that I want to return an error as soon as I get it so that I don't spend time waiting for all the goroutines to finish as it would be inefficient.

I tried adding the select statement but it doesn't work and I can't add the select statement inside the goroutines since I want to exit the for loop and the try function too.

How can I do this?

Here is the code:

package main

import (
    "sync"
    "runtime"
    "fmt"
    "errors"
)

func try() (bool, error) {
    wg := new(sync.WaitGroup)

    s := []int{0,1,2,3,4,5}
    ec := make(chan error)
    
    for i, val := range s {
    /*
        select {
             case err, ok := <-ec:
        if ok {
            println("error 1", err.Error())
            return false, err
        }
            default:
            }
    */
        wg.Add(1)
        i := i
        val := val
        go func() {
            err := func(i int, val int, wg *sync.WaitGroup) error {
                defer wg.Done()
                
                if i == 3 {
                    return errors.New("one error")
                } else {
                    return nil
                }
                
            }(i, val, wg)
            if err != nil {
                ec <- err
                return
            }
        }()
    }
    wg.Wait()
    
    select {
    case err, ok := <-ec:
        if ok {
            println("error 2", err.Error())
            return false, err
        }
    default:
    }
    
    return true, nil
}

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())
    b, e := try()
    if e != nil {
        fmt.Println(e.Error(), b)
    } 
    
}

This is the go playground link


Solution

  • With wg.Wait() before your select statement, you are effectively waiting for all goroutines to return.

    The issue with this is that I want to return an error as soon as I get it

    I assume that with this you mean stopping running goroutines as soon as any one of them returns an error.

    In this case, you could use context.Context to manage cancellation, but even better is an errgroup.Group, which nicely combines context functionality and synchronization:

    Package errgroup provides synchronization, error propagation, and Context cancelation for groups of goroutines working on subtasks of a common task.

    In particular Group.Go:

    The first call to return a non-nil error cancels the group; its error will be returned by Wait.

    import (
        "sync"
        "runtime"
        "fmt"
        "errors"
        "golang.org/x/sync/errgroup"
    )
    
    func try() (bool, error) {
        errg := new(errgroup.Group)
    
        s := []int{0,1,2,3,4,5}
        
        for i, val := range s {       
            i := i
            val := val
    
            errg.Go(func() error {
                return func(i int, val int) error {
                    if i == 3 {
                        return errors.New("one error")
                    } else {
                        return nil
                    }
                }(i, val)
            })
        }
        
        if err := errg.Wait(); err != nil {
            // handle error
        }
        
        return true, nil
    }
    

    https://play.golang.org/p/lSIIFJqXf0W