Go 语言错误处理为什么更推荐使用 pkg/errors 三方库?

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: Go 语言错误处理为什么更推荐使用 pkg/errors 三方库?

介绍

Go 语言项目开发中,我们通常需要在代码逻辑中进行错误处理,Go 官方标准库 errors 为我们提供了一些方法,比如 NewUnwarpIsAs

其中,我们用的最多的是 New,但是,在我们实际 Go 项目开发中,会使用一些分层设计,比如 MVCClean Architecture 等。

在使用分层设计的项目中,如果我们使用 Go 标准库 errors 定义错误,就会遇到错误覆盖的问题。

关于标准库 errors 的错误覆盖问题

Go标准库 errorsNew 方法,只能定义一条简单的错误信息,在分层设计的项目代码中,就会遇到错误覆盖的问题,比如我们本文的示例项目代码,使用的是 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 标准库 errorsNew 方法定义错误,代码片段如下:

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/v4@v4.7.2/echo.go:520
github.com/labstack/echo/v4.(*Echo).ServeHTTP
        /Users/frank/go/pkg/mod/github.com/labstack/echo/v4@v4.7.2/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/errorsWrap 方法,我们再修改一下代码,将 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/v4@v4.7.2/echo.go:520
github.com/labstack/echo/v4.(*Echo).ServeHTTP
        /Users/frank/go/pkg/mod/github.com/labstack/echo/v4@v4.7.2/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 的另外两个方法 WithMessageWithStack 也比较常用,它们分别是在已有的错误信息的基础上,附加新的错误信息和错误的堆栈信息,我们在实际项目开发中,可以按需选择使用合适的方法。

04

总结

本文我们讲述了使用 Go 标准库 errors 进行错误处理的局限性和不足,为了解决它的不足,我们介绍了使用三方库 pkg/errors 替换 Go 标准库 errors,和三方库 pkg/errors 的几个常用方法的使用方式。

关于三方库 pkg/errors 的更多方法,感兴趣的读者朋友们可以阅读文档了解如何使用。

推荐阅读:

参考资料:

  1. https://pkg.go.dev/github.com/pkg/errors@v0.9.1
  2. https://pkg.go.dev/errors
  3. https://dave.cheney.net/2016/06/12/stack-traces-and-the-errors-package
  4. https://morioh.com/p/777d15fe7828
相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
5天前
|
存储 JSON 监控
Viper,一个Go语言配置管理神器!
Viper 是一个功能强大的 Go 语言配置管理库,支持从多种来源读取配置,包括文件、环境变量、远程配置中心等。本文详细介绍了 Viper 的核心特性和使用方法,包括从本地 YAML 文件和 Consul 远程配置中心读取配置的示例。Viper 的多来源配置、动态配置和轻松集成特性使其成为管理复杂应用配置的理想选择。
23 2
|
9天前
|
JavaScript Java Go
探索Go语言在微服务架构中的优势
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出。本文将深入探讨Go语言在构建微服务时的性能优势,包括其在内存管理、网络编程、并发模型以及工具链支持方面的特点。通过对比其他流行语言,我们将揭示Go语言如何成为微服务架构中的一股清流。
|
3天前
|
Go 索引
go语言中的循环语句
【11月更文挑战第4天】
11 2
|
3天前
|
Go C++
go语言中的条件语句
【11月更文挑战第4天】
14 2
|
8天前
|
Ubuntu 编译器 Linux
go语言中SQLite3驱动安装
【11月更文挑战第2天】
30 7
|
8天前
|
关系型数据库 Go 网络安全
go语言中PostgreSQL驱动安装
【11月更文挑战第2天】
38 5
|
8天前
|
安全 Go
用 Zap 轻松搞定 Go 语言中的结构化日志
在现代应用程序开发中,日志记录至关重要。Go 语言中有许多日志库,而 Zap 因其高性能和灵活性脱颖而出。本文详细介绍如何在 Go 项目中使用 Zap 进行结构化日志记录,并展示如何定制日志输出,满足生产环境需求。通过基础示例、SugaredLogger 的便捷使用以及自定义日志配置,帮助你在实际开发中高效管理日志。
25 1
|
7天前
|
程序员 Go
go语言中的控制结构
【11月更文挑战第3天】
84 58
|
6天前
|
监控 Go API
Go语言在微服务架构中的应用实践
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出,成为构建微服务的理想选择。本文将探讨Go语言在微服务架构中的应用实践,包括Go语言的特性如何适应微服务架构的需求,以及在实际开发中如何利用Go语言的特性来提高服务的性能和可维护性。我们将通过一个具体的案例分析,展示Go语言在微服务开发中的优势,并讨论在实际应用中可能遇到的挑战和解决方案。
|
7天前
|
存储 编译器 Go
go语言中的变量、常量、数据类型
【11月更文挑战第3天】
24 9