当前位置: 首页 > 科技观察

Go语言错误处理为什么推荐使用Pkg-Errors三方库?

时间:2023-03-21 20:52:48 科技观察

1。简介在Go语言项目的开发中,我们通常需要处理代码逻辑中的错误。Go官方标准库errors为我们提供了New、Unwarp、Is、As等方法。其中,我们使用最多的是New,但是在我们实际的Go项目开发中,会用到一些分层设计,比如MVC,CleanArchitecture等。在使用分层设计的项目中,如果我们使用Go来定义错误标准库错误,我们会遇到错误覆盖的问题。2.关于标准库errors的错误覆盖Go标准库errors的New方法只能定义简单的错误信息。在层次化设计的项目代码中,会遇到错误覆盖问题,比如我们的文章示例项目代码,使用了CleanArchitecture的层次化设计。项目层级目录:.├──app│└──main.go├──domain│└──user.go├──go.mod├──go.sum└──user├──delivery│└──http│└──user.go├──repository│└──mysql│└──user.go└──usecase└──user.go在示例项目中,我们首先使用Go标准库错误的新方法定义错误,代码片段如下:repositorylayer:func(m*mysqlUserRepository)GetUserById(ctxcontext.Context,user*domain.User)(errerror){_,err=m.DB.Get(user)fmt.printf("mysqlUserRepository||GetUserById()||uid=%v||err=%v\n",user.Id,err)return}用例层:func(u*userUsecase)GetUserById(ctxcontext.Context,user*domain.User)(errerror){ifuser.Id==0{err=errors.New("无效请求参数")}err=u.userRepo.GetUserById(ctx,user)fmt.Printf("userUsecase||GetUserById()||uid=%v||err=%v\n",user.Id,err)return}传递层:func(u*UserHandler)GetUserById(cecho.Context)error{idP,err:=strconv.Atoi(c.Param("id"))iferr!=nil{returnc.JSON(http.StatusNotFound,err)}id:=int64(idP)ctx:=c.Request().Context()user:=&domain.User{Id:id,}err=u.UserUsecase.GetUserById(ctx,user)iferr!=nil{错误=errors.New("UserUsecaseerror")fmt.Printf("UserHandler||GetUserById()||uid=%v||err=%+v\n",id,err)返回c.JSON(http.StatusInternalServerError,err)}returnc.JSON(http.StatusOK,user)}阅读以上三段代码,我们可以发现我们在每一层都有错误处理代码,我们故意使用了错误的请求参数,连接数据库的密码错误,触发应用的错误输出:mysqlUserRepository||GetUserById()||uid=1||err=错误1045:用户'root'@'172.17.0.1'的访问被拒绝(使用密码:YES)userUsecase||GetUserById()||uid=1||err=错误1045:用户'root'@'172.17.0.1'的访问被拒绝(使用密码:YES)UserHandler||GetUserById()||uid=1||err=UserUsecaseerror读取输出结果,可以发现usecase层定义的错误被调用的repository层返回的错误覆盖了;交付层定义的错误被用例层返回的错误覆盖。因为我们每一层都有打印错误,仔细排查后,还是可以定位错误的,但是还是比较繁琐。不仅每一层打印的错误让代码不优雅,而且无法快速定位错误。如何解决这个问题呢?使用第三方库github.com/pkg/errors替换Go标准库错误。3、三方库pkg/errors使用三方库pkg/errors解决了分层设计的项目中调用栈错误信息相互重叠的问题。它可以为我们输出错误的堆栈信息,并且可以在已有的错误信息中添加新的信息,从而解决输出错误信息缺少上下文的问题。我们修改Part02的示例代码,将Go标准库errors替换为三方库pkg/errors。相信细心的读者发现了,因为两个包同名,而且都有New方法,替换也比较方便,直接替换导入的包即可。示例代码:import(//"errors""fmt""github.com/labstack/echo/v4""github.com/pkg/errors""github.com/weirubo/learn_go/lesson41/domain""net/http""strconv")替换了输出:mysqlUserRepository||GetUserById()||uid=0||err=错误1045:用户'root'@'172.17.0.1'的访问被拒绝(使用密码:YES)userUsecase||GetUserById()||uid=0||err=错误1045:用户'root'@'172.17.0.1'的访问被拒绝(使用密码:YES)UserHandler||GetUserById()||uid=0||err=UserUsecase错误github.com/weirubo/learn_go/lesson41/user/delivery/http.(*UserHandler).GetUserById/Users/frank/GolandProjects/learn_go/lesson41/user/delivery/http/user.go:36github.com/labstack/echo/v4.(*Echo).add.func1/Users/frank/go/pkg/mod/github.com/labstack/echo/v4@v4.7.2/echo.go:520github.com/labstack/echo/v4.(*Echo).ServeHTTP/Users/frank/go/pkg/mod/github.com/labstack/echo/v4@v4.7.2/echo.go:630net/http.serverHandler.ServeHTTP/usr/local/go/src/net/http/server.go:2916net/http.(*conn).serve/usr/local/go/src/net/http/server.go:1966runtime.goexit/usr/local/go/src/runtime/asm_amd64.s:1571阅读上面在输出结果中我们可以发现,将错误处理包替换为Go标准库errors和三方库pkg/errors后,输出结果中不仅有Go标准库errors的错误信息,还会输出错误的堆栈信息。至此,我们只是切换了导入的包后,报错信息中包含了错误的堆栈信息。但是,我们的错误覆盖问题还没有解决。我们还是要用到三方库pkg/errors的Wrap方法。让我们再次修改代码。用Wrap方法替换New方法。交付层:...iferr!=nil{//err=errors.New("UserUsecaseerror")err=errors.Wrap(err,"UserUsecaseerror")fmt.Printf("UserHandler||GetUserById()||uid=%v||err=%+v\n",id,err)returnc.JSON(http.StatusInternalServerError,err)}...看了上面的代码,我们修改下交付层的错误处理代码,将New方法替换为Wrap方法,可以在已有错误信息的基础上追加新的错误信息和错误堆栈信息。输出结果:mysqlUserRepository||GetUserById()||uid=0||err=错误1045:用户'root'@'172.17.0.1'的访问被拒绝(使用密码:YES)userUsecase||GetUserById()||uid=0||err=错误1045:用户'root'@'172.17.0.1'的访问被拒绝(使用密码:YES)UserHandler||GetUserById()||uid=0||err=Error1045:Accessdeniedforuser'root'@'172.17.0.1'(usingpassword:YES)UserUsecase错误github.com/weirubo/learn_go/lesson41/user/delivery/http.(*UserHandler).GetUserById/Users/frank/GolandProjects/learn_go/lesson41/user/delivery/http/user.go:37github.com/labstack/echo/v4.(*Echo).add.func1/Users/frank/go/pkg/mod/github.com/labstack/echo/v4@v4.7.2/echo.go:520github.com/labstack/echo/v4.(*Echo).ServeHTTP/Users/frank/go/pkg/mod/github.com/labstack/echo/v4@v4.7.2/echo.go:630net/http.serverHandler.ServeHTTP/usr/local/go/src/net/http/server.go:2916net/http.(*conn).serve/usr/local/go/src/net/http/server.go:1966runtime.goexit/usr/local/go/src/runtime/asm_amd64.s:1571读取上面的输出结果可以发现,delivery层定义的错误信息并没有覆盖调用usecase层方法返回的错误信息.需要注意的是错误信息和堆栈信息是同时输出的。placeholder需要使用%+v,不要在每一层都输出栈信息,会重复打印栈信息。通常的做法是,如果下层打印了栈信息,上层就应该不再打印栈信息了。另外,三方库pkg/errors的另外两个方法WithMessage和WithStack也是常用的。它们分别在已有错误信息的基础上增加新的错误信息和错误堆栈信息。我们在实际项目中开发它们,您可以根据需要选择使用合适的方法。4.总结在本文中,我们描述了使用Go标准库errors进行错误处理的局限性和缺点。为了解决它的缺点,我们介绍了使用三方库pkg/errors来替代Go标准库errors,以及三方库pkg/errors如何使用errors的几种常见方法。三方库pkg/errors的更多方法,有兴趣的读者可以阅读文档了解使用方法。