我们在使用 go 编写代码的时候,在错误处理的时候,经常会写出很多 if err != nil
,其实有些时候我们可以使用一些技巧去避免,本文就来讨论两种常见的避免技巧,内部包装错误和 errgroup。
基本 case 实现
代码语言:javascript复制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 里面我们讨论的基础是,这些启动服务之间没有关联关系,并且只要有其中一个启动失败就直接退出。
内部操作包装实现
代码语言:javascript复制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 实现
代码语言:javascript复制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 要来的方便。
- 避免重复代码的技巧往往就是,抽象后合并实现,同时使用合理的设计模式