Golang 语言怎么处理错误?

2021年2月28日 169点热度 0人点赞 0条评论

大家好,我是 frank。
欢迎大家点击上方蓝色文字「Golang 语言开发栈」关注公众号。

01

介绍

golang 程序大多数是通过 if err != nil 处理错误,在 golang 社区中,有一部分 golang 程序员对此举是持反对观点,他们认为在 golang 代码中存在大量的错误处理代码 if err != nil,使整体代码变得非常不优雅,应该在 golang 中引入其他处理错误的机制,例如 try-catche 或其它此类处理错误的机制。其实,他们忽略了 golang 中一个特别重要的概念,即 errors are values,并且 golang 作者 Rob Pike 也对此问题做出过回应,在 golang 代码中出现重复的错误处理代码 if err != nil,可能是 golang 用户的使用方式有问题。

本文我们主要聊聊在 golang 中,怎么处理错误?

02

golang 定义错误的两种方式

使用 golang 标准库 errors 的 New() 函数,可以定义一个错误类型的变量。

func New(text string) error

New() 函数接收一个 string 类型的文本,返回一个 error 类型的变量。即使给定的文本不同,每次对 New() 函数的调用也会返回不同的错误值。

关于每次调用 New() 函数,都可以返回不同的错误值,golang 是怎么做到的呢?我们通过阅读 golang 的源码,找一下我们的问题答案。

源码 /usr/local/go/src/errors/errors.go

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
 return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
 s string
}

func (e *errorString) Error() string {
 return e.s
}

源码中,我们发现 New() 函数体中的代码是返回一个指针类型 &errorString{text},所以我们的疑问自然有了答案。

那么,golang 中定义错误的另外一种方式是什么?在 golang 标准库 fmt 中,通过调用 Errorf() 函数也可以返回一个 error 类型的错误。

源码 /usr/local/go/src/fmt/errors.go

func Errorf(format string, a ...interface{}) error {
 p := newPrinter()
 p.wrapErrs = true
 p.doPrintf(format, a)
 s := string(p.buf)
 var err error
 if p.wrappedErr == nil {
  err = errors.New(s)
 } else {
  err = &wrapError{s, p.wrappedErr}
 }
 p.free()
 return err
}

03

错误处理方式之“不透明错误处理”

正如我们在文章开篇所述,在 golang 程序中,我们见的最多的错误处理方式就是 if err != nil,此种错误处理方式,错误处理方不关心错误提供方的错误值。因此,我们将此种错误处理方式称为“不透明错误处理”。

示例代码:

err := errors.New("this is a error example")
if err != nil {
 fmt.Println(err)
  return
}

04

golang 1.13 新增 As() 函数

在 golang 1.13 中,新增 As() 函数,当 error 类型的变量是一个包装错误(wrap error)时,它可以顺着错误链(error chain)上所有被包装的错误(wrapped error)的类型做比较,直到找到一个匹配的错误类型,并返回 true,如果找不到,则返回 false。

通常,我们会使用 As() 函数判断一个 error 类型的变量是否为特定的自定义错误类型。

示例代码:

// 自定义的错误类型
type DefineError struct {
 msg string
}

func (d *DefineError) Error() string {
 return d.msg
}

func main() {
  // wrap error
 err1 := &DefineError{"this is a define error type"}
 err2 := fmt.Errorf("wrap err2: %w\n", err1)
 err3 := fmt.Errorf("wrap err3: %w\n", err2)
 var err4 *DefineError
 if errors.As(err3, &err4) {
  // errors.As() 顺着错误链,从 err3 一直找到被包装最底层的错误值 err1,并且将 err3 与其自定义类型 `var err4 *DefineError` 匹配成功。
  fmt.Println("err1 is a variable of the DefineError type")
  fmt.Println(err4 == err1)
  return
 }
 fmt.Println("err1 is not a variable of the DefineError type")
}

05

golang 1.13 新增 Is() 函数

在 Part03 中,我们讲述了“不透明错误处理”的错误处理方式,错误处理方不关心错误提供方的错误值。但是,在错误处理方需要关心错误提供方的错误值时,错误处理方要对错误提供方的错误值进行判定,这就造成了代码的耦合,错误提供方的错误值每次修改,错误处理方都需要跟着做出相应修改。

针对这种情况,golang 一般会采用“哨兵错误处理”的错误处理方式,即定义可导出的错误变量,错误处理方和错误提供方都只操作错误变量,这样做的好处是只需维护错误变量,但是还没有彻底解决问题,如果 error 类型的错误变量是一个包装错误(wrap error),“哨兵错误处理”的错误处理方式也不方便处理该错误。

好在 golang 1.13 新增 Is() 函数,它可以顺着错误链(error chain)上所有被包装的错误(wrapped error)的类型做比较,直到找到一个匹配的错误。

示例代码:

// 哨兵错误处理
var (
 ErrInvalidUser     = errors.New("invalid user")
 ErrNotFoundUser    = errors.New("not found user")
)

func main () {
  err1 := fmt.Errorf("wrap err1: %w\n", ErrInvalidUser)
 err2 := fmt.Errorf("wrap err2: %w\n", err1)
  // golang 1.13 新增 Is() 函数
 if errors.Is(err2, ErrInvalidUser) {
  fmt.Println(ErrInvalidUser)
  return
 }
 fmt.Println("success")
}

06

总结

本文我们开篇先是讲述了 golang 社区中,存在对待 golang 错误处理方式的反对态度的用户,这么一个客观事实。接着,我们介绍了 golang 中的两种定义错误的方式和底层源码实现,和 golang 1.13 中新增的关于错误处理的函数。如果你现在使用的是 golang 1.13 及以上版本,请使用 As() 和 Is() 。

通过阅读源码 /usr/local/go/src/errors/wrap.go,我们可以发现 As() 和 Is() 是通过在错误链中不断调用 Unwrap() 函数,最终找到匹配的错误值。其中,Unwrap() 函数也是在 golang 1.13 中新增的函数。

Unwrap() 函数的源码:

// Unwrap returns the result of calling the Unwrap method on err, if err's
// type contains an Unwrap method returning error.
// Otherwise, Unwrap returns nil.
func Unwrap(err error) error {
 u, ok := err.(interface {
  Unwrap() error
 })
 if !ok {
  return nil
 }
 return u.Unwrap()
}

推荐阅读:

Go 语言学习之错误处理

Golang 语言的标准库 log 包怎么使用?

Golang 语言三方库 lumberjack 日志切割组件怎么使用?

Golang 语言标准库 bytes 包怎么使用?

Golang 语言的标准库 os 包怎么操作目录和文件?

参考资料:
https://golang.org/pkg/errors/

延伸阅读:
https://blog.golang.org/error-handling-and-go
https://blog.golang.org/errors-are-values
https://blog.golang.org/go1.13-errors
https://golang.org/doc/tutorial/handle-errors
https://medium.com/rungo/error-handling-in-go-f0125de052f0
https://www.digitalocean.com/community/tutorials/handling-errors-in-go

图片

扫描二维码,加入微信群

图片

感谢支持,请点「赞」和「在看」⬇️

图片

⬇️更多精彩内容,请点击阅读原文

7990Golang 语言怎么处理错误?

root

这个人很懒,什么都没留下

文章评论