Go 项目分层下的最佳 error 处理方式

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 本文对 Go 项目分层下的最佳 `error` 处理方式进行介绍,并通过使用 github.com/pkg/errors 库中的一些实用函数来提供实现示例。

作者:陈明勇
个人网站:https://chenmingyong.cn
文章持续更新,如果本文能让您有所收获,欢迎关注本号。
微信阅读可搜《Go 技术干货》。这篇文章已被收录于 GitHub https://github.com/chenmingyong0423/blog 欢迎大家 Star 催更并持续关注。

前言

Go 语言中,对于程序中可能出现的问题,比如数据库连接失败,文件读取错误等,都是使用基于内置的 error 接口类型的值来表示和处理错误。而在分层的项目中,如何最佳处理 error成为众多人关注的问题,本文将探讨 Go 项目分层下的最佳 error 处理方式。准备好了吗?准备一杯你最喜欢的饮料或茶,随着本文一探究竟吧。

常见分层下的 error 处理

以典型的 MVC ( daoservicecontroller/middleware) 分层结构举例,常见的错误处理大致如下:

// controller / middleware
res, err := service.GetById(ctx, id)
if err != nil {
   
    log.Errorf(ctx, "service.GetById failed, id=%s, error=%v", err)
    ······
}
······

// service  
article, err := dao.GetById(ctx, id)
if err != nil {
   
    log.Errorf(ctx, fmt.Errorf("dao.GetById failed, error=%v", err))
    return fmt.Errorf("dao.GetById failed, error=%v", err)
}
······

// dao
······
if err != nil {
   
    log.Errorf(ctx, fmt.Errorf("GetById failed, id=%s, error=%v", id, err))
    return fmt.Errorf("GetById failed, id=%s, error=%v", id, err)
}
······

以上错误处理的方式,在每一层都打印一条错误日志,然后对得到的 error 进行二次封装。虽然以上处理方式可以使我们在查看日志时方便故障排查和问题定位,同时提供了错误的上下文信息,但也存在以下问题:

  • 每层都打印日志,带来了大量日志;
  • 字符串拼接费时费力,缺乏统一规范,可能导致理解困难;
  • 通过字符串拼接,获得新 error,破坏了原始 error,会导致等值判定失败,难以获取详细的堆栈关联。

    分层下的最佳 error 处理方式

    遵循以下建议,我们可以更好地处理 error
  • 1、一个 error,应该只被处理一次
  • 2、让 error 包含更多的信息
  • 3、原始 error,应保证完整性,不被破坏
  • 4、error 需要被日志记录

什么意思呢?为了确保 error 处理的有效性,对于某一层来说,应该保证每个错误只被处理一次,要么打印 error 信息,要么将其传递给上一层,而不是每一层都独立打印 error 信息。

同时,在传递错误给上一层时,应该附带有用的额外信息,并确保不破坏原始错误的完整性,以保证错误的可追溯性。最后,通过记录错误日志可以帮助我们进行问题排查。

Go 项目分层下的最佳 error 处理方式-1.png

如图所示:

Dao 层遇到原始错误 Original Error 后,我们可以将其与需要的额外信息封装,组成一个新的 error ,然后传递给上一层,逐层附加信息,直至传递到 controller 层,最终得到一个全新的 error,其中包含 Original error 和每一层添加的额外信息。

通过最终得到的这个 error,我们可以像剥洋葱一样逐层解开,追溯到 Original error ,并获取我们所需的信息。如果 controller 是最顶层,我们可以打印完整的错误信息,然后获取 Original Error,并打印其所包含的 堆栈信息

Wrap error

尽管前面已经探讨了分层下的最佳 error 处理方式,但我们会发现官方标准库errors 所提供的函数并不能满足我们的需求,我们不能借助现有函数对原始错误附加额外信息且不破坏其完整性。在这种情况下,我们可以借助第三方库 github.com/pkg/errors 来完成我们的需求。

github.com/pkg/errors 提供了很多实用的函数,例如:

  • Wrap(err error, message string) error:该函数基于原始错误 err,返回一个带有堆栈跟踪信息和附加信息 message 的新 error
  • Wrapf(err error, format string, args ...interface{}) error: 和上面的函数功能是一样的,只不过可以对附加信息进行格式化封装
  • WithMessage(err error, message string) error:该函数基于原始错误 err,返回一个附加信息 message 的新 error
  • WithMessagef(err error, format string, args ...interface{}) error: 和上面的函数功能是一样的,只不过可以对附加信息进行格式化封装
  • Cause(err error) error:该函数用于提取 err 中的原始 error,它会递归地检查 error,直到找到最底层的原始 error,如果存在的话

了解了以上函数的功能,我们来看看项目分层下最佳 error 的具体实现。

// controller / middleware  
res, err := service.GetById(ctx, id)  
if err != nil {
     
  log.Errorf(ctx, "service.GetById failed, original error: %T %v", errors.Cause(err), errors.Cause(err))  
  log.Errorf(ctx, "stack trace: \n%+v\n", err)  
······  
}  
······  

// service  
article, err := dao.GetById(ctx, id)  
if err != nil {
     
  return errors.WithMessage(err, "dao.GetById failed")  
}  
······  

// dao  
······  
if err != nil {
     
  return errors.Wrapf(err, "GetById failed, id=%s, error=%v", id, err)  
}  
······

当在 Dao 层遇到原始错误 Original Error 后,使用 errors.Wrap() 对错误进行封装。这个封装操作可以在保留根因(Origin error)的同时,提供堆栈信息,并添加额外的上下文信息,然后将封装后的错误传递给上一层处理。

service 层接收到 error 之后,使用 errors.WithMessage() 函数,将额外的信息附加到错误上,并继续将错误向上层传递,直至到达 controller 层。在 controller 层,我们可以打印出根因的类型、信息以及堆栈信息,以便更好地进行问题排查。

Go 项目分层下的最佳 error 处理实现-2.png

小结

本文对 Go 项目分层下的最佳 error 处理方式进行介绍,并通过使用 github.com/pkg/errors 库中的一些实用函数来提供实现示例。

尽管本文基于 MVC 分层结构进行介绍,但实际上大多数项目的分层结构可能各不相同,因此在确定错误处理方式和策略时需要考虑具体情况。然而,我相信通过参考本文提出的四点建议和实现示例或其他更好的建议,一定能够确定最佳的错误处理方式。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
8天前
|
JSON 中间件 Go
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
本文详细介绍了如何在Go项目中集成并配置Zap日志库。首先通过`go get -u go.uber.org/zap`命令安装Zap,接着展示了`Logger`与`Sugared Logger`两种日志记录器的基本用法。随后深入探讨了Zap的高级配置,包括如何将日志输出至文件、调整时间格式、记录调用者信息以及日志分割等。最后,文章演示了如何在gin框架中集成Zap,通过自定义中间件实现了日志记录和异常恢复功能。通过这些步骤,读者可以掌握Zap在实际项目中的应用与定制方法
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
|
13天前
|
测试技术 Shell Go
go 语言优雅地处理 error
go 语言优雅地处理 error
|
22天前
|
存储 Linux Go
GO 项目依赖管理:go module总结
GO 项目依赖管理:go module总结
30 0
|
2月前
|
SQL 关系型数据库 测试技术
技术经验分享:Go开源项目
技术经验分享:Go开源项目
25 0
|
7天前
|
JSON 缓存 监控
go语言后端开发学习(五)——如何在项目中使用Viper来配置环境
Viper 是一个强大的 Go 语言配置管理库,适用于各类应用,包括 Twelve-Factor Apps。相比仅支持 `.ini` 格式的 `go-ini`,Viper 支持更多配置格式如 JSON、TOML、YAML
go语言后端开发学习(五)——如何在项目中使用Viper来配置环境
|
12天前
|
算法 程序员 编译器
Go deadcode:查找没意义的死代码,对于维护项目挺有用!
Go deadcode:查找没意义的死代码,对于维护项目挺有用!
|
17天前
|
缓存 JavaScript 前端开发
为开源项目 go-gin-api 增加 WebSocket 模块
为开源项目 go-gin-api 增加 WebSocket 模块
24 2
|
17天前
|
Prometheus Cloud Native NoSQL
Go - 一个对新手很友好的项目(带界面)
Go - 一个对新手很友好的项目(带界面)
26 1
|
17天前
|
SQL NoSQL Go
Go - 实现项目内链路追踪(二)
Go - 实现项目内链路追踪(二)
22 1
|
1月前
|
前端开发 JavaScript Go