2

Go Error 嵌套到底是怎么实现的?

 2 years ago
source link: https://developer.51cto.com/art/202201/699174.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Go Error 嵌套到底是怎么实现的?

Go Error 的设计哲学是 「Errors Are Values」。这句话应该怎么理解呢?翻译起来挺难的。不过从源码的角度来看,好像更容易理解其背后的含义。

作者:AlwaysBeta来源:今日头条|2022-01-14 17:01

Go Error 的设计哲学是 「Errors Are Values」。

这句话应该怎么理解呢?翻译起来挺难的。不过从源码的角度来看,好像更容易理解其背后的含义。

Go Error 源码很简单,寥寥几行:

  1. // src/builtin/builtin.go 
  2. type error interface { 
  3.  Error() string 

error 是一个接口类型,只需要实现 Error() 方法即可。在 Error() 方法中,就可以返回自定义结构体的任意内容。

下面首先说说如何创建 error。

创建 Error

创建 error 有两种方式,分别是:

  • errors.New();
  • fmt.Errorf()。

errors.New()

errors.New() 的使用延续了 Go 的一贯风格,New 一下就可以了。

举一个例子:

  1. package main 
  2. import ( 
  3.  "errors" 
  4.  "fmt" 
  5. func main() { 
  6.  err := errors.New("这是 errors.New() 创建的错误") 
  7.  fmt.Printf("err 错误类型:%T,错误为:%v\n", err, err) 
  8. err 错误类型:*errors.errorString,错误为:这是 errors.New() 创建的错误 

这段代码唯一让人困惑的地方可能就是错误类型了,但没关系。只要看一下源码,就瞬间迎刃而解。

源码如下:

  1. // src/errors/errors.go 
  2. // New returns an error that formats as the given text. 
  3. // Each call to New returns a distinct error value even if the text is identical. 
  4. func New(text string) error { 
  5.  return &errorString{text} 
  6. // errorString is a trivial implementation of error. 
  7. type errorString struct { 
  8.  s string 
  9. func (e *errorString) Error() string { 
  10.  return e.s 

可以看到,errorString 是一个结构体,实现了 Error() 方法,New 函数直接返回 errorString 指针。

这种用法很简单,但不实用。假如我还想返回程序的上下文信息,它就没辙了。

下面看第二种方式。

fmt.Errorf()

还是先看一个例子:

  1. package main 
  2. import ( 
  3.  "database/sql" 
  4.  "fmt" 
  5. func foo() error { 
  6.  return sql.ErrNoRows 
  7. func bar() error { 
  8.  return foo() 
  9. func main() { 
  10.  err := bar() 
  11.  if err == sql.ErrNoRows { 
  12.   fmt.Printf("data not found, %+v\n", err) 
  13.   return 
  14.  if err != nil { 
  15.   fmt.Println("Unknown error") 
  16. data not found, sql: no rows in result set 

这个例子输出了我们想要的结果,但是还不够。

一般情况下,我们会通过使用 fmt.Errorf() 函数,附加上我们想添加的文本信息,使返回内容更明确,处理起来更灵活。

所以,foo() 函数会改成下面这样:

  1. func foo() error { 
  2.    return fmt.Errorf("foo err, %v", sql.ErrNoRows) 

这时问题就出现了,经过 fmt.Errorf() 的封装,原始 error 类型发生了改变,这就导致 err == sql.ErrNoRows 不再成立,返回信息变成了 Unknown error。

如果想根据返回的 error 类型做不同处理,就无法实现了。

因此,Go 1.13 为我们提供了 wrapError 来处理这个问题。

Wrap Error

看一个例子:

  1. package main 
  2. import ( 
  3.  "fmt" 
  4. type myError struct{} 
  5. func (e myError) Error() string { 
  6.  return "Error happended" 
  7. func main() { 
  8.  e1 := myError{} 
  9.  e2 := fmt.Errorf("E2: %w", e1) 
  10.  e3 := fmt.Errorf("E3: %w", e2) 
  11.  fmt.Println(e2) 
  12.  fmt.Println(e3) 
  13. /* output 
  14. E2: Error happended 
  15. E3: E2: Error happended 

乍一看好像好没什么区别,但背后的实现原理却并不相同。

Go 扩展了 fmt.Errorf() 函数,增加了一个 %w 标识符来创建 wrapError。

  1. // src/fmt/errors.go 
  2. func Errorf(format string, a ...interface{}) error { 
  3.  p := newPrinter() 
  4.  p.wrapErrs = true 
  5.  p.doPrintf(format, a) 
  6.  s := string(p.buf) 
  7.  var err error 
  8.  if p.wrappedErr == nil { 
  9.   err = errors.New(s) 
  10.  } else { 
  11.   err = &wrapError{s, p.wrappedErr} 
  12.  p.free() 
  13.  return err 

当使用 w% 时,函数会返回 &wrapError{s, p.wrappedErr},wrapError 结构体定义如下:

  1. // src/fmt/errors.go 
  2. type wrapError struct { 
  3.  msg string 
  4.  err error 
  5. func (e *wrapError) Error() string { 
  6.  return e.msg 
  7. func (e *wrapError) Unwrap() error { 
  8.  return e.err 

实现了 Error() 方法,说明它是一个 error,而 Unwrap() 方法是为了获取被封装的 error。

  1. // src/errors/wrap.go 
  2. func Unwrap(err error) error { 
  3.  u, ok := err.(interface { 
  4.   Unwrap() error 
  5.  if !ok { 
  6.   return nil 
  7.  return u.Unwrap() 

它们之间的关系是这样的:

Go Error 嵌套到底是怎么实现的?

因此,我们可以使用 w% 将上文中的程序进行改造,使其内容输出更丰富。

  1. package main 
  2. import ( 
  3.  "database/sql" 
  4.  "errors" 
  5.  "fmt" 
  6. func bar() error { 
  7.  if err := foo(); err != nil { 
  8.   return fmt.Errorf("bar failed: %w", foo()) 
  9.  return nil 
  10. func foo() error { 
  11.  return fmt.Errorf("foo failed: %w", sql.ErrNoRows) 
  12. func main() { 
  13.  err := bar() 
  14.  if errors.Is(err, sql.ErrNoRows) { 
  15.   fmt.Printf("data not found,  %+v\n", err) 
  16.   return 
  17.  if err != nil { 
  18.   fmt.Println("Unknown error") 
  19. /* output 
  20. data not found,  bar failed: foo failed: sql: no rows in result set 

终于有了让人满意的输出结果,每个函数都增加了必要的上下文信息,而且也符合对错误类型的判断。

errors.Is() 函数用来判断 err 以及其封装的 error 链中是否包含目标类型。这也就解决了上文提出的无法判断错误类型的问题。

其实,Go 目前对 Error 的处理方式也是充满争议的。不过,官方团队正在积极和社区交流,提出改进方法。相信在不久的将来,一定会找到更好的解决方案。

现阶段来说,大部分团队可能会选择 github.com/pkg/errors 包来进行错误处理。如果感兴趣的话,可以学学看。

好了,本文就到这里吧。关注我,带你通过问题读 Go 源码。

源码地址:https://github.com/yongxinz/gopher

参考文章:

https://chasecs.github.io/posts/the-philosophy-of-go-error-handling/

https://medium.com/@dche423/golang-error-handling-best-practice-cn-42982bd72672

https://www.flysnow.org/2019/09/06/go1.13-error-wrapping.html

【编辑推荐】

【责任编辑:武晓燕 TEL:(010)68476606】
点赞 0

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK