Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cancel goroutines on first error using errgroup

Tags:

go

I'm trying to cancel remaining goroutines after an error being encountered in one of them or at least cancel the fetch function call in them.

func fetch(n int, fail bool) (string, error) {
  time.Sleep(time.Duration(n) * time.Second)
  if fail {
    return "", errors.New("an error") //fmt.Errorf("an error")
  } else {
    return "Hello", nil
  }
}

func getData(ctx context.Context, ch chan string, n int, fail bool) error {
  fmt.Println("fetch data" + strconv.Itoa(n))

  select {
  case <-ctx.Done():
    return ctx.Err()
  default:
    if response, err := fetch(n, fail); err != nil {
        fmt.Println("error encountered at ", strconv.Itoa(n))
        return err
    } else {
        ch <- response
        fmt.Println("fetched data" + strconv.Itoa(n))
        return nil
    }
  }
}

func main() {
  res, err := func() (string, error) {
    ctx := context.Background()
    g, ctx := errgroup.WithContext(ctx)

    ch1 := make(chan string, 1)
    ch2 := make(chan string, 1)
    ch3 := make(chan string, 1)

    g.Go(func() error {
        defer close(ch1)
        return getData(ctx, ch1, 1, true)
    })

    g.Go(func() error {
        defer close(ch2)
        return getData(ctx, ch2, 2, false)
    })

    g.Go(func() error {
        defer close(ch3)
        return getData(ctx, ch3, 3, false)
    })

    result := <-ch1 + <-ch2 + <-ch3
    return result, g.Wait()
  }()

  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println(res)
  }
}

The output should look like:

fetch data1
fetch data2
fetch data3
error encountered at  1
an error

but actually is:

fetch data3
fetch data1
fetch data2
error encountered at  1
fetched data2
fetched data3
an error

What I understand the errgroup should be doing is closing the context when an error is found, and then <-ctx.Done() should return ctx.Err(), but instead It's like the context is never done and the fetch function keeps being called.

I think the problem is in the select statement. Any idea of what i'm doing wrong or missing? Link to playground

like image 370
cetaphil Avatar asked Oct 14 '25 05:10

cetaphil


1 Answers

Pass the context to the fetch function and exit on cancel:

func fetch(ctx context.Context, n int, fail bool) (string, error) {  
    select {
    case <-time.After(time.Duration(n) * time.Second):
        if fail {
            return "", errors.New("an error")
        } else {
            return "Hello", nil
        }
    case <-ctx.Done():
        fmt.Println("canceled")
        return "CXL", ctx.Err()
    }
}

There's no need for the channel stuff and GetData. Just assign results from fetch to variables.

ctx := context.Background()
g, ctx := errgroup.WithContext(ctx)

var r1, r2, r3 string

g.Go(func() error {
    var err error
    r1, err = fetch(ctx, 1, true)
    return err
})

g.Go(func() error {
    var err error
    r2, err = fetch(ctx, 2, false)
    return err
})

g.Go(func() error {
    var err error
    r3, err = fetch(ctx, 3, false)
    return err
})

err := g.Wait()
fmt.Printf("err: %v, 1: %s; 2: %s; 3: %s\n", err, r1, r2, r3)

Run it on the playground.

like image 104
user13631587 Avatar answered Oct 16 '25 19:10

user13631587



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!