首页 > 解决方案 > Cancel goroutines on first error using errgroup

问题描述

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

标签: go

解决方案


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.


推荐阅读