1. 程式人生 > >go中errgroup原始碼解讀

go中errgroup原始碼解讀

- [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`進行超時,取消或者一些異常的情況