前言
在 Go
语言中,对于程序中可能出现的问题,比如数据库连接失败,文件读取错误等,都是使用基于内置的 error
接口类型的值来表示和处理错误。而在分层的项目中,如何最佳处理 error
成为众多人关注的问题,本文将探讨 Go
项目分层下的最佳 error
处理方式。准备好了吗?准备一杯你最喜欢的饮料或茶,随着本文一探究竟吧。
常见分层下的 error 处理
以典型的 MVC
( dao
→ service
→ controller/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
信息。
同时,在传递错误给上一层时,应该附带有用的额外信息,并确保不破坏原始错误的完整性,以保证错误的可追溯性。最后,通过记录错误日志可以帮助我们进行问题排查。
如图所示:
在 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% vn", 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
处理方式进行介绍,并通过使用 github.com/pkg/errors
库中的一些实用函数来提供实现示例。
尽管本文基于 MVC
分层结构进行介绍,但实际上大多数项目的分层结构可能各不相同,因此在确定错误处理方式和策略时需要考虑具体情况。然而,我相信通过参考本文提出的四点建议和实现示例或其他更好的建议,一定能够确定最佳的错误处理方式。
我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!