errGroup in GoLang explained

  sonic0002        2023-05-27 23:58:20       7,216        0          English  简体中文  Tiếng Việt 

今天,让我们讨论一下经常使用的errGroup的用法和适用场景。为什么它有用?通常,当我们使用goroutine时,我们无法返回值。如果想传递goroutine执行的结果,通常需要使用channel。如果想在打开的goroutine执行过程中遇到错误并停止工作时知道,并且需要知道错误值,那么errGroup包就适合了。

errGroup用法

首先需要下载并安装该包。

go get -u golang.org/x/sync

一个使用示例如下

package main

import (
    "fmt"
    "log"
    "net/http"

    "golang.org/x/sync/errgroup"
)

func main() {
    eg := errgroup.Group{}
    eg.Go(func() error {
        return getPage("https://blog.kennycoder.io")
    })
    eg.Go(func() error {
        return getPage("https://google.com")
    })
    if err := eg.Wait(); err != nil {
        log.Fatalf("get error: %v", err)
    }
}

func getPage(url string) error {
    resp, err := http.Get(url)
    if err != nil {
        return err
    }
    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("fail to get page: %s, wrong statusCode: %d", url, resp.StatusCode)
    }
    log.Printf("success get page %s", url)
    return nil
}
  • 首先,创建一个group结构体。这个结构体本身不需要设置任何字段,所以创建一个空结构体就可以了。
  • group结构体只提供两个函数,GoWait,实际上和waitGroup非常类似。
  • Go函数接受一个函数作为参数并返回一个错误。这个函数实际上就是你想在goroutine中执行的函数。在上面的例子中,getPage被放在Go中。getPage的实现很简单,它调用http.Get(url),如果statusCode不是200则返回错误。否则,它会写入日志以指示执行成功。
  • 最后,调用Wait意味着它将开始阻塞,类似于waitGroup的Wait方法。它将等待你打开的所有goroutine完成执行后再退出Wait。不同之处在于Wait会返回一个错误,这个错误来自你的goroutine之一返回的错误。

在上面的例子中,Wait()本身没有返回错误,因为所有网页都可以正常访问。如果将其中一个URL更改为不存在的URL,则会看到效果。

2021/10/03 11:52:23 success get page https://google.com
2021/10/03 11:52:23 get error: Get "https://kenny.example.com": dial tcp: lookup kenny.example.com: no such host
exit status 1

如果你的两个goroutine都尝试访问不存在的URL:

2021/10/03 11:53:52 get error: Get "https://kenny.example.com": dial tcp: lookup kenny.example.com: no such host
exit status 1

你会发现最后只打印了一个错误,因为errGroup只存储一个goroutine的错误。哪个goroutine的错误被存储取决于哪个goroutine先遇到错误并被存储。后续goroutine的错误将不会被存储。

你可能会觉得你想要的是知道所有错误的结果,只给我一个goroutine的错误并没有帮助。在上面的例子中,它确实不是理想的,因为你不知道哪些URL无法访问。所以个人认为,errGroup适合的场景是你想执行的任务相同或性质相同。例如,如果你有多个goroutine需要访问同一个服务来获取不同的信息。因此,当其中一个goroutine失败时,最可能的原因是网络问题,其他访问同一服务的goroutine也将无法成功访问。此外,如果你想要的是当一个goroutine失败时,即使其他goroutine已经成功完成,也没有帮助,那么errGroup非常适合。

但是,仅仅使用一个空的Group结构体并没有帮助。

errGroup运行时,即使你的goroutine之一遇到错误,它也不会取消其他goroutine。这意味着其他goroutine无法及时取消,也无法知道goroutine是否正确退出。

errGroup考虑到了这种情况,所以它选择使用context方法来取消其他goroutine。

一个例子

func main() {
  eg, ctx := errgroup.WithContext(context.Background())
  eg.Go(func() error {
    for i := 0; i < 10; i++ {
      select {
      case <-ctx.Done():
        log.Printf("goroutine should cancel")
        return nil
      default:
        if err := getPage("https://blog.kennycoder.io"); err != nil {
          return err
        }
        time.Sleep(1 * time.Second)
      }
    }
    return nil
  })
  eg.Go(func() error {
    for i := 0; i < 10; i++ {
      select {
      case <-ctx.Done():
        log.Printf("goroutine should cancel")
        return nil
      default:
        if err := getPage("https://google.com"); err != nil {
          return err
        }
        time.Sleep(1 * time.Second)
      }
    }
    return nil
  })
  if err := eg.Wait(); err != nil {
    log.Fatalf("get error: %v", err)
  }
}
  • errGroup提供WithContext将你的父context放入,并返回一个group结构体和context。这个context实际上是一个cancel context。
  • 获得cancel context后,可以将其放入Go函数中,并使用select <-ctx.Done()来知道是否被取消从而结束goroutine。在这个场景中,我访问URL十次。如果有一次出错,我就返回err,如果我收到ctx.Done(),我就返回nil来结束goroutine。我错误地访问其中一个URL来查看效果:
2021/10/03 12:11:40 success get page https://google.com
2021/10/03 12:11:41 goroutine should cancel
2021/10/03 12:11:41 get error: Get "https:kenny.example.com": http: no Host in request URL
exit status 1

可以看到,第二个goroutine成功访问了一次URL,但是它必须打印出日志并结束第二个goroutine,因为它在第二次成功访问URL之前收到了ctx.Done。最后,Wait错误打印出了第一个goroutine的错误。

这种方法确保当一个goroutine遇到错误时,它也会通知其他goroutine结束它们的工作,并确保goroutine可以正常退出。

errGroup内部是如何工作的

让我们来看看它的结构。

type Group struct {
  cancel func()

  wg sync.WaitGroup

  errOnce sync.Once
  err     error
}

这是Group结构体的结构,它显示有一个cancel func()用于前面提到的WithContextWaitGroup的出现表明errGroup也是通过WaitGroup实现的,Once是为了只接受一个错误的存在。err是最终Wait返回的错误值。

以下是Go函数

func (g *Group) Go(f func() error) {
  g.wg.Add(1)

  go func() {
    defer g.wg.Done()

    if err := f(); err != nil {
      g.errOnce.Do(func() {
        g.err = err
        if g.cancel != nil {
          g.cancel()
        }
      })
    }
  }()
}

通过wg.Add(1),内部打开一个goroutine来执行传入的函数,并检查返回值是否有错误。如果有错误,则使用errOnce.Do来存储错误。因为Once的特点是无论你传入什么函数,Once都只会执行一次,即使多个goroutine返回错误。因此,g.err = err只会设置一次,并且一旦成功设置,它的值就不会改变。

最后,它还会检查cancel是否为nil。如果它不是nil,这意味着什么?这意味着errGroup是使用WithContext创建的,所以cancel不会是nil。这时,g.cancel()将被调用来为你取消其他goroutine,并且ctx.Done()将会有一个值。

并且因为它通过WaitGroup实现,所以每个goroutine完成工作后都需要调用g.wg.Done()来递减它。

以下是Wait函数:

func (g *Group) Wait() error {
  g.wg.Wait()
  if g.cancel != nil {
    g.cancel()
  }
  return g.err
}

同样,调用g.wg.Wait(),这会导致阻塞效果。所有goroutine完成后,它将检查cancel是否为nil,并以相同的方式取消其他goroutine。这样做的原因更多的是为了取消这个context。毕竟,这个cancel context是为这个errGroup存在的,所有任务完成后应该重置它。

func WithContext(ctx context.Context) (*Group, context.Context) {
  ctx, cancel := context.WithCancel(ctx)
  return &Group{cancel: cancel}, ctx
}

可以看到,确实建立了一个cancel context,并将ctx返回给客户端使用,并将cancel func存储起来。

总而言之,errGroup的设计非常简单,巧妙地利用WaitGroupContext实现了等待所有goroutine并获取错误的能力,并利用Context允许客户端设计goroutine退出的实现。

CONCURRENCY  GOLANG  ERRGROUP  WAITHROUP 

       

  RELATED


  0 COMMENT


No comment for this article.



  RANDOM FUN

Lies of multiple cores