大家好,我是 frank。
欢迎大家点击标题下方蓝色文字「Golang 语言开发栈」关注公众号。
设为星标,第一时间接收推送文章。
文末扫码,加群一起学 Golang 语言。
01
介绍
Go 语言项目开发中,我们通常需要在代码逻辑中进行错误处理,Go 官方标准库 errors
为我们提供了一些方法,比如 New
,Unwarp
,Is
和 As
。
其中,我们用的最多的是 New
,但是,在我们实际 Go 项目开发中,会使用一些分层设计,比如 MVC
,Clean Architecture
等。
在使用分层设计的项目中,如果我们使用 Go 标准库 errors
定义错误,就会遇到错误覆盖的问题。
02
关于标准库 errors 的错误覆盖问题
Go标准库 errors
的 New
方法,只能定义一条简单的错误信息,在分层设计的项目代码中,就会遇到错误覆盖的问题,比如我们本文的示例项目代码,使用的是 Clean Architecture
分层设计。
项目分层目录:
.
├── app
│ └── main.go
├── domain
│ └── user.go
├── go.mod
├── go.sum
└── user
├── delivery
│ └── http
│ └── user.go
├── repository
│ └── mysql
│ └── user.go
└── usecase
└── user.go
在示例项目中,我们先使用 Go 标准库 errors
的 New
方法定义错误,代码片段如下:
repository 层:
func (m *mysqlUserRepository) GetUserById(ctx context.Context, user *domain.User) (err error) {
_, err = m.DB.Get(user)
fmt.Printf("mysqlUserRepository || GetUserById() || uid=%v || err=%v\n", user.Id, err)
return
}
usecase 层:
func (u *userUsecase) GetUserById(ctx context.Context, user *domain.User) (err error) {
if user.Id == 0 {
err = errors.New("invalid request parameter")
}
err = u.userRepo.GetUserById(ctx, user)
fmt.Printf("userUsecase || GetUserById() || uid=%v || err=%v\n", user.Id, err)
return
}
delivery 层:
func (u *UserHandler) GetUserById(c echo.Context) error {
idP, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.JSON(http.StatusNotFound, err)
}
id := int64(idP)
ctx := c.Request().Context()
user := &domain.User{
Id: id,
}
err = u.UserUsecase.GetUserById(ctx, user)
if err != nil {
err = errors.New("UserUsecase error")
fmt.Printf("UserHandler || GetUserById() || uid=%v || err=%+v\n", id, err)
return c.JSON(http.StatusInternalServerError, err)
}
return c.JSON(http.StatusOK, user)
}
阅读上面三段代码,我们可以发现,我们在每层中都有错误处理的代码,我们故意使用错误的请求参数,并将数据库连接的密码写错,触发应用程序的错误。
输出结果:
mysqlUserRepository || GetUserById() || uid=1 || err=Error 1045: Access denied for user 'root'@'172.17.0.1' (using password: YES)
userUsecase || GetUserById() || uid=1 || err=Error 1045: Access denied for user 'root'@'172.17.0.1' (using password: YES)
UserHandler || GetUserById() || uid=1 || err=UserUsecase error
阅读输出结果,我们可以发现,usecase 层定义的错误,被调用的 repository 层返回错误覆盖;delivery 层定义的错误将 usecase 层返回的错误覆盖。
因为我们在每层都打印了错误,仔细排查,还是可以定位到错误,但是还是比较繁琐,不仅每层打印错误使代码不够优雅,而且也不能快速定位到错误。
怎么解决这个问题呢?使用三方库 github.com/pkg/errors
替换 Go 标准库 errors
。
03
三方库 pkg/errors
使用三方库 pkg/errors
可以解决在分层设计的项目中调用堆栈的错误信息互相覆盖,可以为我们输出错误的堆栈信息,可以在已有错误信息的基础上附加新的错误信息,从而解决输出的错误信息缺失上下文的问题。
我们修改一下 Part 02 的示例代码,将 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=Error 1045: Access denied for user 'root'@'172.17.0.1' (using password: YES)
userUsecase || GetUserById() || uid=0 || err=Error 1045: Access denied for user 'root'@'172.17.0.1' (using password: YES)
UserHandler || GetUserById() || uid=0 || err=UserUsecase error
github.com/weirubo/learn_go/lesson41/user/delivery/http.(*UserHandler).GetUserById
/Users/frank/GolandProjects/learn_go/lesson41/user/delivery/http/user.go:36
github.com/labstack/echo/v4.(*Echo).add.func1
/Users/frank/go/pkg/mod/github.com/labstack/echo/[email protected]/echo.go:520
github.com/labstack/echo/v4.(*Echo).ServeHTTP
/Users/frank/go/pkg/mod/github.com/labstack/echo/[email protected]/echo.go:630
net/http.serverHandler.ServeHTTP
/usr/local/go/src/net/http/server.go:2916
net/http.(*conn).serve
/usr/local/go/src/net/http/server.go:1966
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1571
阅读上面的输出结果,我们可以发现错误处理包由 Go 标准库 errors
替换为三方库 pkg/errors
后,输出结果不仅有 Go 标准库 errors
的错误信息,还输出了错误的堆栈信息。
目前为止,我们只是切换了一下导入的包,错误信息就包含了错误的堆栈信息,但是,我们的错误覆盖问题还没有得到解决,我们还需要使用三方库 pkg/errors
的 Wrap
方法,我们再修改一下代码,将 New
方法替换为 Wrap
方法。
delivery 层:
...
if err != nil {
// err = errors.New("UserUsecase error")
err = errors.Wrap(err, "UserUsecase error")
fmt.Printf("UserHandler || GetUserById() || uid=%v || err=%+v\n", id, err)
return c.JSON(http.StatusInternalServerError, err)
}
...
阅读上面这段代码,我们修改 delivery 层的错误处理代码,将 New
方法替换为 Wrap
方法,它可以在已有错误信息的基础上,附加新的错误信息和错误的堆栈信息。
输出结果:
mysqlUserRepository || GetUserById() || uid=0 || err=Error 1045: Access denied for user 'root'@'172.17.0.1' (using password: YES)
userUsecase || GetUserById() || uid=0 || err=Error 1045: Access denied for user 'root'@'172.17.0.1' (using password: YES)
UserHandler || GetUserById() || uid=0 || err=Error 1045: Access denied for user 'root'@'172.17.0.1' (using password: YES)
UserUsecase error
github.com/weirubo/learn_go/lesson41/user/delivery/http.(*UserHandler).GetUserById
/Users/frank/GolandProjects/learn_go/lesson41/user/delivery/http/user.go:37
github.com/labstack/echo/v4.(*Echo).add.func1
/Users/frank/go/pkg/mod/github.com/labstack/echo/[email protected]/echo.go:520
github.com/labstack/echo/v4.(*Echo).ServeHTTP
/Users/frank/go/pkg/mod/github.com/labstack/echo/[email protected]/echo.go:630
net/http.serverHandler.ServeHTTP
/usr/local/go/src/net/http/server.go:2916
net/http.(*conn).serve
/usr/local/go/src/net/http/server.go:1966
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1571
阅读以上输出结果,我们可以发现在 delivery 层定义的错误信息,没有再覆盖调用 usecase
层方法返回的错误信息,二者都被正常输出。
需要注意的是,同时输出错误信息和堆栈信息,占位符需要使用 %+v
,也不要在每层都输出堆栈信息,这样会重复打印堆栈信息,通常做法是如果下层打印了堆栈信息,上层就不要再打印堆栈信息。
此外,三方库 pkg/errors
的另外两个方法 WithMessage
和 WithStack
也比较常用,它们分别是在已有的错误信息的基础上,附加新的错误信息和错误的堆栈信息,我们在实际项目开发中,可以按需选择使用合适的方法。
04
总结
本文我们讲述了使用 Go 标准库 errors
进行错误处理的局限性和不足,为了解决它的不足,我们介绍了使用三方库 pkg/errors
替换 Go 标准库 errors
,和三方库 pkg/errors
的几个常用方法的使用方式。
关于三方库 pkg/errors
的更多方法,感兴趣的读者朋友们可以阅读文档了解如何使用。
推荐阅读:
-
Go 语言怎么使用对称加密? -
Go 微服务工具包 Go kit 怎么集成 gRPC? -
Golang 语言的多种变量声明方式和使用场景 -
Golang 语言 vendor 在 GOPATH 和 Modules 中的区别 -
Golang 语言怎么使用 panic 函数?
参考资料:
-
https://pkg.go.dev/github.com/pkg/[email protected] -
https://pkg.go.dev/errors -
https://dave.cheney.net/2016/06/12/stack-traces-and-the-errors-package -
https://morioh.com/p/777d15fe7828
扫描二维码或回复「微信群」,加入微信群
点「赞」和「在看」是最大的支持?
?更多精彩内容,请点击「阅读原文」
文章评论