乐趣区

关于golang:Golang并发编程包之-errgroup

原文链接:并发编程包之 errgroup

前言

哈喽,大家好,我是 asong,明天给大家介绍一个并发编程包errgroup,其实这个包就是对sync.waitGroup 的封装。咱们在之前的文章—— 源码分析 sync.WaitGroup(文末思考题你能解释一下吗?),从源码层面剖析了 sync.WaitGroup 的实现,应用 waitGroup 能够实现一个 goroutine 期待一组 goroutine 干活完结,更好的实现了工作同步,然而 waitGroup 却无奈返回谬误,当一组 Goroutine 中的某个 goroutine 出错时,咱们是无奈感知到的,所以 errGroupwaitGroup进行了一层封装,封装代码仅仅不到 50 行,上面咱们就来看一看他是如何封装的?

errGroup如何应用

老规矩,咱们先看一下 errGroup 是如何应用的,后面吹了这么久,先来验验货;

以下来自官网文档的例子:

var (Web   = fakeSearch("web")
    Image = fakeSearch("image")
    Video = fakeSearch("video")
)

type Result string
type Search func(ctx context.Context, query string) (Result, error)

func fakeSearch(kind string) Search {return func(_ context.Context, query string) (Result, error) {return Result(fmt.Sprintf("%s result for %q", kind, query)), nil
    }
}

func main() {Google := func(ctx context.Context, query string) ([]Result, error) {g, ctx := errgroup.WithContext(ctx)

        searches := []Search{Web, Image, Video}
        results := make([]Result, len(searches))
        for i, search := range searches {
            i, search := i, search // https://golang.org/doc/faq#closures_and_goroutines
            g.Go(func() error {result, err := search(ctx, query)
                if err == nil {results[i] = result
                }
                return err
            })
        }
        if err := g.Wait(); err != nil {return nil, err}
        return results, nil
    }

    results, err := Google(context.Background(), "golang")
    if err != nil {fmt.Fprintln(os.Stderr, err)
        return
    }
    for _, result := range results {fmt.Println(result)
    }

}

下面这个例子来自官网文档,代码量有点多,然而外围次要是在 Google 这个闭包中,首先咱们应用 errgroup.WithContext 创立一个 errGroup 对象和 ctx 对象,而后咱们间接调用 errGroup 对象的 Go 办法就能够启动一个协程了,Go办法中曾经封装了 waitGroup 的管制操作,不须要咱们手动增加了,最初咱们调用 Wait 办法,其实就是调用了 waitGroup 办法。这个包不仅缩小了咱们的代码量,而且还减少了错误处理,对于一些业务能够更好的进行并发解决。

赏析errGroup

数据结构

咱们先看一下 Group 的数据结构:

type Group struct {cancel func() // 这个存的是 context 的 cancel 办法

    wg sync.WaitGroup // 封装 sync.WaitGroup

    errOnce sync.Once // 保障只承受一次谬误
    err     error // 保留第一个返回的谬误
}

办法解析

func WithContext(ctx context.Context) (*Group, context.Context)
func (g *Group) Go(f func() error)
func (g *Group) Wait() error

errGroup总共只有三个办法:

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

这个办法只有两步:

  • 应用 contextWithCancel()办法创立一个可勾销的Context
  • 创立 cancel() 办法赋值给 Group 对象
  • 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()
                }
            })
        }
    }()}

Go办法中运行步骤如下:

  • 执行 Add() 办法减少一个计数器
  • 开启一个协程,运行咱们传入的函数 f,应用waitGroupDone()办法管制是否完结
  • 如果有一个函数 f 运行出错了,咱们把它保存起来,如果有 cancel() 办法,则执行 cancel() 勾销其余goroutine

这里大家应该会好奇为什么应用 errOnce,也就是sync.Once,这里的目标就是保障获取到第一个出错的信息,防止被前面的Goroutine 的谬误笼罩。

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

总结一下 wait 办法的执行逻辑:

  • 调用 waitGroupWait()期待一组 Goroutine 的运行完结
  • 这里为了保障代码的健壮性,如果后面赋值了 cancel,要执行cancel() 办法
  • 返回错误信息,如果有 goroutine 呈现了谬误才会有值

小结

到这里咱们就剖析完了 errGroup 包,总共就 1 个构造体和 3 个办法,了解起来还是比较简单的,针对下面的知识点咱们做一个小结:

  • 咱们能够应用 withContext 办法创立一个可勾销的 Group,也能够间接应用一个零值的Groupnew一个 Group,不过间接应用零值的Groupnew进去的 Group 呈现谬误之后就不能取消其余 Goroutine 了。
  • 如果多个 Goroutine 呈现谬误,咱们只会获取到第一个出错的 Goroutine 的错误信息,晚于第一个出错的 Goroutine 的错误信息将不会被感知到。
  • errGroup中没有做 panic 解决,咱们在 Go 办法中传入 func() error 办法时要保障程序的健壮性

踩坑日记

应用 errGroup 也并不是一番风顺的,我之前在我的项目中应用 errGroup 就呈现了一个BUG,把它分享进去,防止踩坑。

这个需要是这样的 (并不是实在业务场景,由asong 虚构的):开启多个 Goroutine 去缓存中设置数据,同时开启一个 Goroutine 去异步写日志,很快我的代码就写进去了:

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

    // 独自开一个协程去做其余的事件,不参加 waitGroup
    go WriteChangeLog(ctx)

    for i:=0 ; i< 3; i++{g.Go(func() error {return errors.New("拜访 redis 失败 \n")
        })
    }
    if err := g.Wait();err != nil{fmt.Printf("appear error and err is %s",err.Error())
    }
    time.Sleep(1 * time.Second)
}

func WriteChangeLog(ctx context.Context) error {
    select {case <- ctx.Done():
        return nil
    case <- time.After(time.Millisecond * 50):
        fmt.Println("write changelog")
    }
    return nil
}
// 运行后果
appear error and err is 拜访 redis 失败

代码没啥问题吧,然而日志始终没有写入,排查了良久,终于找到问题起因。起因就是这个ctx

因为这个 ctxWithContext办法返回的一个带勾销的 ctx,咱们把这个ctx 当作父 context 传入 WriteChangeLog 办法中了,如果 errGroup 勾销了,也会导致上下文的 context 都勾销了,所以 WriteChangelog 办法就始终执行不到。

这个点是咱们在日常开发中想不到的,所以须要留神一下~。

总结

因为最近看很多敌人都不晓得这个库,所以明天就把他分享进去了,封装代码仅仅不到 50 行,真的是很厉害,如果让你来封装,你能封装的更好吗?

欢送关注公众号:【Golang 梦工厂】

举荐往期文章:

  • 学习 channel 设计:从入门到放弃
  • 编程模式之 Go 如何实现装璜器
  • Go 语言中 new 和 make 你应用哪个来分配内存?
  • 源码分析 panic 与 recover,看不懂你打我好了!
  • 空构造体引发的大型打脸现场
  • [面试官:你能聊聊 string 和[]byte 的转换吗?](https://mp.weixin.qq.com/s/jz…
  • 面试官:两个 nil 比拟后果是什么?
  • 面试官:你能用 Go 写段代码判断以后零碎的存储形式吗?
  • 赏析 Singleflight 设计
退出移动版