我们在使用 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 还提供了 SetLimit
和 TryGo
方法,通过设定一个并发的上限来确保并发的任务数不会超过限制条件。
总结
- 本文主要记录了 errgroup 的基本使用,使用明显能比自己亲自使用 waitgroup 要来的方便。
- 避免重复代码的技巧往往就是,抽象后合并实现,同时使用合理的设计模式