go中errgroup原始碼解讀
阿新 • • 發佈:2021-03-08
- [errgroup](#errgroup)
- [前言](#%E5%89%8D%E8%A8%80)
- [如何使用](#%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8)
- [實現原理](#%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86)
- [WithContext](#withcontext)
- [Go](#go)
- [Wait](#wait)
- [錯誤的使用](#%E9%94%99%E8%AF%AF%E7%9A%84%E4%BD%BF%E7%94%A8)
- [總結](#%E6%80%BB%E7%BB%93)
## errgroup
### 前言
來看下errgroup的實現
### 如何使用
```go
func main() {
var eg errgroup.Group
eg.Go(func() error {
return errors.New("test1")
})
eg.Go(func() error {
return errors.New("test2")
})
if err := eg.Wait(); err != nil {
fmt.Println(err)
}
}
```
類比於`waitgroup`,`errgroup`增加了一個對`goroutine`錯誤收集的作用。
不過需要注意的是:
`errgroup`返回的第一個出錯的`goroutine`丟擲的`err`。
`errgroup`中還可以加入`context`
```go
func main() {
eg, ctx := errgroup.WithContext(context.Background())
eg.Go(func() error {
// test1函式還可以在啟動很多goroutine
// 子節點都傳入ctx,當test1報錯,會把test1的子節點一一cancel
return test1(ctx)
})
eg.Go(func() error {
return test1(ctx)
})
if err := eg.Wait(); err != nil {
fmt.Println(err)
}
}
func test1(ctx context.Context) error {
return errors.New("test2")
}
```
### 實現原理
程式碼很簡單
```go
type Group struct {
// 一個取消的函式,主要來包裝context.WithCancel的CancelFunc
cancel func()
// 還是藉助於WaitGroup實現的
wg sync.WaitGroup
// 使用sync.Once實現只輸出第一個err
errOnce sync.Once
// 記錄下錯誤的資訊
err error
}
```
還是在WaitGroup的基礎上實現的
### WithContext
```go
// 返回一個被context.WithCancel重新包裝的ctx
func WithContext(ctx context.Context) (*Group, context.Context) {
ctx, cancel := context.WithCancel(ctx)
return &Group{cancel: cancel}, ctx
}
```
裡面使用了`context`,通過`context.WithCancel`對傳入的context進行了包裝
當`WithCancel`函式返回的`CancelFunc`被呼叫或者是父節點的`done channel`被關閉(父節點的 CancelFunc 被呼叫),此 context(子節點)的 `done channel` 也會被關閉。
`errgroup`把返回的`CancelFunc`包進了自己的`cancel`中,來實現對使用`errgroup`的`ctx`啟動的`goroutine`的取消操作。
### Go
```go
// 啟動取消阻塞的goroutine
// 記錄第一個出錯的goroutine的err資訊
func (g *Group) Go(f func() error) {
// 藉助於waitgroup實現
g.wg.Add(1)
go func() {
defer g.wg.Done()
// 執行出錯
if err := f(); err != nil {
// 通過sync.Once記錄下第一個出錯的err資訊
g.errOnce.Do(func() {
g.err = err
// 如果包裝了cancel,也就是context的CancelFunc,執行退出操作
if g.cancel != nil {
g.cancel()
}
})
}
}()
}
```
1、藉助於`waitgroup`實現對`goroutine`阻塞;
2、通過`sync.Once`記錄下,第一個出錯的`goroutine`的錯誤資訊;
3、如果包裝了`context`的`CancelFunc`,在出錯的時候進行退出操作。
### Wait
```go
// 阻塞所有的通過Go加入的goroutine,然後等待他們一個個執行完成
// 然後返回第一個出錯的goroutine的錯誤資訊
func (g *Group) Wait() error {
// 藉助於waitgroup實現
g.wg.Wait()
// 如果包裝了cancel,也就是context的CancelFunc,執行退出操作
if g.cancel != nil {
g.cancel()
}
return g.err
}
```
1、藉助於`waitgroup`實現對`goroutine`阻塞;
2、如果包裝了`context`的`CancelFunc`,在出錯的時候進行退出操作;
3、丟擲第一個出錯的`goroutine`的錯誤資訊。
### 錯誤的使用
不過工作中發現一個`errgroup`錯誤使用的例子
```go
func main() {
eg := errgroup.Group{}
var err error
eg.Go(func() error {
// 處理業務
err = test1()
return err
})
eg.Go(func() error {
// 處理業務
err = test1()
return err
})
if err = eg.Wait(); err != nil {
fmt.Println(err)
}
}
func test1() error {
return errors.New("test2")
}
```
很明顯err被資源競爭了
```go
$ go run -race main.go
==================
WARNING: DATA RACE
Write at 0x00c0000801f0 by goroutine 8:
main.main.func2()
/Users/yj/Go/src/Go-POINT/sync/errgroup/main.go:23 +0x97
...
```
### 總結
`errgroup`相比比較簡單,不過需要先弄明白`waitgroup`,`context`以及`sync.Once`,主要是藉助這幾個元件來實現的。
`errgroup`可以帶攜帶`context`,如果包裝了`context`,會使用`context.WithCancel`進行超時,取消或者一些異常的情況