前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go语言技巧 - 2.【错误处理】谈谈Go Error的前世今生

Go语言技巧 - 2.【错误处理】谈谈Go Error的前世今生

作者头像
junedayday
发布2021-08-05 11:27:12
4500
发布2021-08-05 11:27:12
举报
文章被收录于专栏:Go编程点滴

从Go 2 Error Proposal谈起

Goerror的处理一直都是很大的争议点,这点官方也已多次发文,并在2019年1月推出了一篇Proposal,有兴趣的可以点击链接细细品读。

官方原文链接

下面,我会结合Proposal原文,发表一些自己的看法(会带上主观意见),欢迎讨论。

目标

这篇Proposal有一句话很好地解释了对error的期许:

making errors more informative for both programs and people

错误不仅是告诉机器怎么做的,也是告诉人发生了什么问题。

回顾

先让我们一起简单地回顾一下error的现状,来更好地理解这个 more informative 指的是什么。

原始的error定义为:

代码语言:javascript
复制
type error interface {
  Error() string
}

这里面的包含信息很少:一个Error() 的方法,即用字符串返回对应的错误信息。

最常用的error相关方法是2种:

  1. 创建error - fmt.Errorf,它是针对Error()方法返回的字符串进行加工,如附带一些参数信息(暂不讨论%w这个wrap错误的实现)
  2. 使用error - 由于我们将error的输出结果定义为字符串,所以使用error时,一旦涉及到细节,就只能使用一些string的方法了

举个具体的例子:

代码语言:javascript
复制
func main() {
 // 假设 readFile 存在于第三方或公用的库,我们没有权限修改、或者修改它的影响面很大
 _, err := readFile("test")

 // 错误中包含业务逻辑:
 // 1. 文件不存在时,认为是 正常
 // 2. 其余报错时,认为是 异常
 if err != nil {
  if strings.Index(err.Error(), "no such file or directory") >= 0 {
   log.Println("file not exist")
   os.Exit(0)
  }
  log.Println("open file error")
  os.Exit(1)
 }
}

func readFile(fileName string) ([]byte, error) {
 b, err := ioutil.ReadFile(fileName)
 if err != nil {
  return nil, fmt.Errorf("read file %s error %v", fileName, err)
 }
 return b, nil
}

这里存在3个明显的问题:

  1. 破坏性 - fmt.Errorf 破坏了原有的error,将它从一个 具体对象 转化为 扁平的 string,再填充到了新的error中。所以,通过fmt.Errorf处理后的error,都只传递了一个string的信息
  2. 实现僵化 - "no such file or directory" 这个错误信息用的是硬编码,对第三方readFile的内容有强依赖,不灵活
  3. 排查问题效率低 - 可以通过日志组件了解到error在main函数哪行发生,但无法知道错误从readFile中的哪行返回过来的

其中第一个破坏性的问题,其实就是破坏了error这个interface背后的具体实现,违背了面向对象的继承原则。

Handle Errors Only Once

在工程中,为了解决 排查问题效率低 这个问题,有一个很常见的做法(以上面的readFile为例):

代码语言:javascript
复制
func readFile(fileName string) ([]byte, error) {
 b, err := ioutil.ReadFile(fileName)
 if err != nil {
  log.Printf("read file %s error %v", fileName, err)
  return nil, fmt.Errorf("read file %s error %v", fileName, err)
 }
 return b, nil
}

没错,就是 打印错误并返回。有大量排查问题经验的同学,对此肯定是深恶痛绝: 一个错误能找到N处打印,看得人眼花缭乱

这里违背了一个关键性的原则:对错误只进行一次处理,处理完之后就不要再往上抛了,而打印错误也是一种处理。

结合三种具体的场景,我们分析一下:

  1. 一个程序模块内,error不断往上抛,最上层处理;
  2. 一个公共的工具包中,error不记录,传给调用方处理;
  3. 一个RPC模块的调用中,error可以记录,作为debug信息,而具体的处理仍应交给调用方。

示例参考文章

  • https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully
  • https://www.orsolabs.com/post/go-errors-and-logs/

理论实现

那么,怎么样的error才是合适的呢?我们分两个角度来看这个error

  1. 对程序来说,error要包含错误细节:如错误类型、错误码等,方便在模块间传递;
  2. 对人来说,error要包含代码信息:如相关的调用参数、运行信息,方便查问题;

用原文一句话来归纳:hide implementation details from programs while displaying them for diagnosis

  • Wrap - 隐藏实现,针对代码调用时的堆栈信息
  • Is/As - 展示细节,针对底层真正实现的数据结构

当前实现

Go语言发展多年,已经有了很多关于error的处理方法,但大多为过渡方案,我就不一一分析了。

这里我以 github.com/pkg/errors 为例,也是这个官方Proposal的重点参考对象,简单地分享一下大致实现思路。

代码量并不多,大家可以自行阅读源码:

New 产生错误的堆栈信息

代码语言:javascript
复制
func New(message string) error {
 return &fundamental{
  msg:   message,
  stack: callers(),
 }
}

type fundamental struct {
 msg string
 *stack
}

关键点 stack保存了错误产生的堆栈信息,如函数名、代码行

Wrap 包装错误

代码语言:javascript
复制
func Wrap(err error, message string) error {
 if err == nil {
  return nil
 }
 err = &withMessage{
  cause: err,
  msg:   message,
 }
 return &withStack{
  err,
  callers(),
 }
}

关键点 将错误包装出一个全新的堆栈。一般只用于对外接口产生错误时,包括标准库、RPC。

WithMessage 添加普通信息

代码语言:javascript
复制
func WithMessage(err error, message string) error {
 if err == nil {
  return nil
 }
 return &withMessage{
  cause: err,
  msg:   message,
 }
}

关键点 添加错误信息,增加一个普通的堆栈打印

Is 解析Sentinel错误、即全局错误变量

代码语言:javascript
复制
func Is(err, target error) bool { return stderrors.Is(err, target) }

func Is(err, target error) bool {
 if target == nil {
  return err == target
 }

 isComparable := reflectlite.TypeOf(target).Comparable()
 for {
  if isComparable && err == target {
   return true
  }
  if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
   return true
  }
  // TODO: consider supporing target.Is(err). This would allow
  // user-definable predicates, but also may allow for coping with sloppy
  // APIs, thereby making it easier to get away with them.
  if err = Unwrap(err); err == nil {
   return false
  }
 }
}

关键点 反复Unwrap、提取错误,解析并对比错误类型

As - 提取出具体的错误数据结构

代码语言:javascript
复制
func As(err error, target interface{}) bool { return stderrors.As(err, target) }

func As(err error, target interface{}) bool {
 if target == nil {
  panic("errors: target cannot be nil")
 }
 val := reflectlite.ValueOf(target)
 typ := val.Type()
 if typ.Kind() != reflectlite.Ptr || val.IsNil() {
  panic("errors: target must be a non-nil pointer")
 }
 if e := typ.Elem(); e.Kind() != reflectlite.Interface && !e.Implements(errorType) {
  panic("errors: *target must be interface or implement error")
 }
 targetType := typ.Elem()
 for err != nil {
  if reflectlite.TypeOf(err).AssignableTo(targetType) {
   val.Elem().Set(reflectlite.ValueOf(err))
   return true
  }
  if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
   return true
  }
  err = Unwrap(err)
 }
 return false
}

关键点 反复Unwrap、提取错误,提取底层的实现类型

小结

Go语言对error的定义很简单,虽然带来了灵活性,但也导致处理方式泛滥,一如当年的Go语言的版本管理。如今的go mod版本管理机制已经”一统江湖“,随着大家对error这块的不断深入,Error Handling也总会达成共识。

接下来,我会结合实际代码样例,写一个具体工程中 Error Handling 的操作方法,提供一定的参考。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-05-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Go编程点滴 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 从Go 2 Error Proposal谈起
    • 目标
      • 回顾
        • Handle Errors Only Once
          • 理论实现
            • 当前实现
              • New 产生错误的堆栈信息
              • Wrap 包装错误
              • WithMessage 添加普通信息
              • Is 解析Sentinel错误、即全局错误变量
            • As - 提取出具体的错误数据结构
              • 小结
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档