我们在使用 go 编写代码的时候,在错误处理的时候,经常会写出很多 if err != nil ,其实有些时候我们可以使用一些技巧去避免,本文就来讨论两种常见的避免技巧,内部包装错误和 errgroup。

基本 case 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main  

import "fmt"

func StartUserService() error {
fmt.Println("start user service")
return nil
}

func StartGoodsService() error {
fmt.Println("start goods service")
return nil
}

func StartOrderService() error {
fmt.Println("start order service")
return nil
}

func main() {
err := StartUserService()
if err != nil {
panic(err)
}
err = StartGoodsService()
if err != nil {
panic(err)
}
err = StartOrderService()
if err != nil {
panic(err)
}
}

这是一个我们常常见到的情况,就是对于多个不同的方法进行调用,比如启动不同的服务,然后每次启动都会返回一个错误,我们都需要对错误进行处理,那么我们如何去优化这个代码呢?

为了简化问题,这个 case 里面我们讨论的基础是,这些启动服务之间没有关联关系,并且只要有其中一个启动失败就直接退出。

内部操作包装实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package main  

import "fmt"

type ServiceManager struct {
err error
}

type StartFn func() (err error)

func (s *ServiceManager) Start(sf StartFn) {
if s.err != nil {
return
}
s.err = sf()
}

func (s *ServiceManager) Err() error {
return s.err
}

func StartUserService() error {
fmt.Println("start user service")
return nil
}

func StartGoodsService() error {
fmt.Println("start goods service")
return nil
}

func StartOrderService() error {
fmt.Println("start order service")
return nil
}

func main() {
sm := &ServiceManager{}

sm.Start(StartUserService)
sm.Start(StartGoodsService)
sm.Start(StartOrderService)

if err := sm.Err(); err != nil {
panic(err)
}
}

当我们遇到重复代码想要合并的时候,第一个想法应该就是抽象,将不同的样子的方法进行抽象,抽象成一个接口。这样抽象之后我们往往就可以通过一次代码来实现相同的功能。

上述的代码中,将启动抽象,并且将错误包装到了一个结构的内部,这也是我们常用的一个技巧,这样的好处在于,在主函数中就没有额外的处理逻辑,只需要无脑的进行调用就可以了。

errgroup 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main  

import (
"context"
"fmt"
"golang.org/x/sync/errgroup"
)

func StartUserService() error {
fmt.Println("start user service")
return nil
}

func StartGoodsService() error {
fmt.Println("start goods service")
return nil
}

func StartOrderService() error {
fmt.Println("start order service")
return nil
}

func main() {
gp, _ := errgroup.WithContext(context.Background())
gp.Go(StartUserService)
gp.Go(StartGoodsService)
gp.Go(StartOrderService)
if err := gp.Wait(); err != nil {
panic(err)
}
}

另一种更为通用的方式是用 errgroup,其实它的原理也是类似的,只不过使用 goroutine 去运行了各个子任务,然后等待子任务全部完成,内部就是通过 waitgroup 实现的。并且当有任意一个出现错误时就会记录错误,最终在 wait 返回。

errgroup 源码见:https://cs.opensource.google/go/x/sync/+/master:errgroup/errgroup.go

扩展

errgroup 还提供了 SetLimitTryGo 方法,通过设定一个并发的上限来确保并发的任务数不会超过限制条件。

总结

  • 本文主要记录了 errgroup 的基本使用,使用明显能比自己亲自使用 waitgroup 要来的方便。
  • 避免重复代码的技巧往往就是,抽象后合并实现,同时使用合理的设计模式