Golang 中 Error 的设计及最佳实践

2023年 8月 16日 63.0k 0

如果你对于 Go 的 Error 设计不太熟悉也不习惯,为什么许多接口都需要返回 error 接口类型的值呢?什么时候该处理 error,什么时候该抛出 error,什么时候又该忽略 error ?Go 设计者又为什么要这样设计 error 呢?想必刚接触 Golang 的同学也会和我一样有类似的疑惑,在读了 TGPL 以及 Go Blog 相关的章节/内容后,我尝试回答一下这些问题。

在第 1 、2小节我将尝试回答 error 是什么,它是如何设计的,以及为什么这样设计。

在第 3 小节我将回答在 Coding 时,如何处理错误。

Error 是什么?

在 Go built-in 包中,Error 被设计为一个接口。

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
   Error() string
}

Go 的设计理念是:失败(failure)只是一种常见的行为。

因此对于那些失败被视作理所当然的函数可以返回一个额外的结果——error,通常是最后一个返回值。而如果失败只有一种可能的原因,那么只需要返回一个 bool 值即可。

上述做法在 Go 的源码或接口设计中很常见。举两个例子:

以常见的 Reader 接口为例, Read 方法读取至多 len(p) 个字节到 p 中,返回读取的字节数 n 和读取过程中可能发生的错误 err。

type Reader interface {
   Read(p []byte) (n int, err error)
}

当我们使用 map 时经常遇到的一种情况是:确定某个键是否在 map 中。但是 map 在该键不存在时也会返回默认值,此时可以使用带 bool 返回值的形式:

if val, ok := m["key"]; ok {
    // do something
} else {
    // do other things
}

Error 的设计

Go 的错误处理设计与其他语言的异常不同。Go 中的 error 就是一个普通的值对象,而其他语言如 Java 中的 Exception 将会造成程序控制流的终止和其他行为,Exception 与普通的值不同。虽然 Go 也有类似的异常机制 —— panic,但它仅用于报告完全无法预料的错误(可能有 Bug),而不应该是一个健壮程序应该返回的程序错误(这一点与 Java 等语言不同)。

关于 Go 为什么这样设计的原因,Kernighan 在 《The Go Programming Language》中给出解释:"The reason for this design is that exceptions tend to entangle the description of an error with the control flow required to handle it, often leading to an undesirable outcome: routine errors are reported to the end user in the form of an incomprehensible stack trace, full of information about the structure of the program but lacking intelligible context about what went wrong"。

即:因为异常会将错误的描述和处理错误的控制流纠缠在一起,通常会导致程序错误以一种难以理解的栈追踪的方式被报告给终端用户,这种方式充满了程序结构的信息,但是缺少关于哪里出错的易于理解的上下文信息。

相反,Go 程序使用普通的程序控制流机制如 if 以及 return 来对 error 作出响应,这种设计虽然要求 Gophers 更加关注错误处理逻辑,但这正是它想做到的点。即“好的程序应该考虑到所有可能的错误,并且对其进行处理”。

🤔 Go 将 error 设计为一个接口,只需要实现 Error() string 方法,返回有意义、简练的错误描述信息即可。这也使得我们可以以任何的方式来自定义错误。

Tips: 建议在底层只需要返回清晰地错误信息,每一层包裹一些重要并且简洁的上下文信息,并且最终在程序的顶层或者某一个不得不处理的层级处理该错误。

正是这种方式,在 Go 中也将这种层层包裹的错误称之为错误链。由此,在 Go 1.13 之后出现了一些新的设计以支持这种错误链的处理方式。其中最简单的错误链就是如下所述的层层包裹的文本信息(或者程序调用栈信息)

genesis: crashed: no parachute: G-switch failed: bad relay orientation

Error 处理策略

  • 最常见的做法是传递错误。即将被调用程序产生的错误传递给调用方,由上层决定如何处理,并且如果需要的话可以附加一些本程序所知的上下文信息。
obj, err := doSomething()
if err != nil {
    return err
}
// do otherthings
  • 第二种,对于那些表示短暂的、难以预测的错误,可以去重试该操作,当然要施加一定的重试次数限制
for i := 0; i < times; i++ {
    res, err := run()
    if err == nil {
        return res
    }
    // do something like log or metrics
}

  • 第三种,如果无法继续往下执行,调用方打印错误信息并且优雅地结束程序
if err := initT(); err != nil {
    panic("something wrong") // though this way is not elegant
}
  • 第四种,在某些情况错误并不致命,也可以只是记录下错误并且继续往下执行。这种情况,最多导致程序缺少部分功能,但总比什么都不做要好。
obj, err := doSomething()
if err != nil {
    logs.CtxInfo(ctx, "something wrong but it doesnot cause serious consequences")
}
// and continue to do something
  • 最后一种,调用方确信不可能发生的错误或者即使发生了也不会有任何问题,可以忽略它。
// could not encode failed
bytes, _ := json.Marshal(obj)

最后,Go 的错误处理比较特别,一般在检查错误之后,先处理失败情况然后再处理成功情形——"Happy Path"。可以保证所有错误均被处理之后再开心的处理正常情形(提醒 Programmer 不要忘记处理异常情况),并且可以减少缩进层级(在其他语言也被称为 "Guard"模式)。

obj, err := getObj()
if err != nil {
    // do some err handling policy
    return fmt.Errorf("could not get obj, err = %v", err)
}
// happy path

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论