原文链接:并发编程包之 errgroup
前言
哈喽,大家好,我是
asong
,明天给大家介绍一个并发编程包errgroup
,其实这个包就是对sync.waitGroup
的封装。咱们在之前的文章—— 源码分析 sync.WaitGroup(文末思考题你能解释一下吗?),从源码层面剖析了sync.WaitGroup
的实现,应用waitGroup
能够实现一个goroutine
期待一组goroutine
干活完结,更好的实现了工作同步,然而waitGroup
却无奈返回谬误,当一组Goroutine
中的某个goroutine
出错时,咱们是无奈感知到的,所以errGroup
对waitGroup
进行了一层封装,封装代码仅仅不到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
}
这个办法只有两步:
- 应用
context
的WithCancel()
办法创立一个可勾销的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
,应用waitGroup
的Done()
办法管制是否完结 - 如果有一个函数
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
办法的执行逻辑:
- 调用
waitGroup
的Wait()
期待一组Goroutine
的运行完结 - 这里为了保障代码的健壮性,如果后面赋值了
cancel
,要执行cancel()
办法 - 返回错误信息,如果有
goroutine
呈现了谬误才会有值
小结
到这里咱们就剖析完了 errGroup
包,总共就 1
个构造体和 3
个办法,了解起来还是比较简单的,针对下面的知识点咱们做一个小结:
- 咱们能够应用
withContext
办法创立一个可勾销的Group
,也能够间接应用一个零值的Group
或new
一个Group
,不过间接应用零值的Group
和new
进去的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
。
因为这个 ctx
是WithContext
办法返回的一个带勾销的 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 设计