Golang 错误处理

169 阅读3分钟

错误处理

Go 语言通过内置的错误接口提供了非常简单的错误处理机制

error 类型是一个接口类型,定义如下

type error interface {
    Error() string
}

/* 使用 errors.New 返回错误信息 */
erros.New("error:is a error info")

/* 使用 fmt.Errorf 返回错误信息 */
fmt.Errorf("decompress %v: %v", name, err)

Go 1.13 内置支持

import (
   "database/sql"
   "errors"
   "fmt"
)

func bar() error {
   if err := foo(); err != nil {                  // 需要判断错误是否为空
      return fmt.Errorf("bar failed: %w", foo())  // 引入了一个新的 fmt 格式化动词: %w,使用 Is/As 进行判断
   }

   return nil
}

func foo() error {
   return fmt.Errorf("foo failed: %w", sql.ErrNoRows) // 引入了一个新的 fmt 格式化动词: %w,使用 Is/As 进行判断
}

func main() {
   err := bar()

   if errors.Is(err, sql.ErrNoRows) {            // 使用Is函数,类似 哨兵错误(sentinel error)比较
      fmt.Printf("data not found,  %+v\n", err)
      return
   }
   
   var ErrSql *sql.ErrNoRows
   if errors.As(err, &ErrSql) {            // 使用As函数,类似 类型断言(type assertion)
      fmt.Printf("data not found,  %+v\n", err)  // 不支持调用栈详情
      return
   }

   if err != nil {
      // unknown error
   }
}

/* Outputs:
data not found,  bar failed: foo failed: sql: no rows in result set
*/

ps:
使用 : %w,如果没有冒号,或 : %w 不位于于格式化字符串的结尾,或冒号与百分号之间没有空格,包装将失效且不报错

Unwrap

func main() {
     e := errors.New("原始错误e")
     w := fmt.Errorf("Wrap了一个错误: %w", e)
     fmt.Println(errors.Unwrap(w))
}

ps:
通过errors.Unwrap(w)后,返回的其实是个e,也就是被嵌套的那个error。 这里需要注意的是,嵌套可以有很多层,我们调用一次errors.Unwrap函数只能返回最外面的一层error,如果想获取更里面的,需要调用多次errors.Unwrap函数。最终如果一个error不是warpping error,那么返回的是nil

哨兵错误比较

/* 将错误与已知的前哨值(sentinel error)进行比较 */

var ErrNotFound = errors.New("not found")

if  err == ErrNotFound {
    // do something
}

类型断言比较

/* 错误值可以是满足语言定义的 error 接口的任何类型,可以使用类型断言(type assertion)或类型开关(type switch)来判断错误值 */

type ErrNotFound struct {
    Name string
}

func (e *ErrNotFound) Error() string {
    Return e.Name + ": not found"
}

if e, ok := err.(*ErrNotFound); ok {
    // do something
}

golang.org/x/xerrors

import (
   "database/sql"
   "fmt"
   "golang.org/x/xerrors"
)

func bar() error {
   if err := foo(); err != nil {                     // 需要判断错误是否为空
      return xerrors.Errorf("bar failed: %w", foo())     // 引入了一个新的 fmt 格式化动词: %w,使用 Is/As 进行判断
   }

   return nil
}

func foo() error {
   return xerrors.Errorf("foo failed: %w", sql.ErrNoRows)  // 引入了一个新的 fmt 格式化动词: %w,使用 Is/As 进行判断
}

func main() {
   err := bar()

   if xerrors.Is(err, sql.ErrNoRows) {        // 使用Is函数,类似 哨兵错误(sentinel error)比较
      fmt.Printf("data not found, %v\n", err)  // 使用 %v 作为格式化参数,那么错误信息会保持一行, 其中依次包含调用栈的上下文文本
      fmt.Printf("%+v\n", err)  // 使用 %+v ,则会输出完整的调用栈详情
      return
   }

   if err != nil {
      // unknown error
   }
}

/* Outputs:data not found, bar failed: foo failed: sql: no rows in result set
bar failed:
    main.bar
        /usr/four/main.go:12
  - foo failed:
    main.foo
        /usr/four/main.go:18
  - sql: no rows in result set
*/

github.com/pkg/errors

import (
   "database/sql"
   "fmt"
   "github.com/pkg/errors"
)

func foo() error {
   return errors.Wrap(sql.ErrNoRows, "foo failed")
}

func bar() error {
   return errors.WithMessage(foo(), "bar failed")
}

/*****************/
ps:
Wrap 方法用来包装底层错误,增加上下文文本信息并附加调用栈。 一般用于包装对第三方代码(标准库或第三方库)的调用。

WithMessage 方法仅增加上下文文本信息,不附加调用栈。 如果确定错误已被 Wrap 过或不关心调用栈,可以使用此方法。 注意:不要反复 Wrap ,会导致调用栈重复

Cause  方法用来判断底层错误
/****************/

func main() {
   err := bar()

   if errors.Cause(err) == sql.ErrNoRows {
      fmt.Printf("data not found, %v\n", err)  // 使用 %v 作为格式化参数,那么错误信息会保持一行, 其中依次包含调用栈的上下文文本
      fmt.Printf("%+v\n", err)    // 使用 %+v ,则会输出完整的调用栈详情
      return
   }

   if err != nil {
      // unknown error
   }
}

/*Output:data not found, bar failed: foo failed: sql: no rows in result set
sql: no rows in result set
foo failed
main.foo
    /usr/three/main.go:11
main.bar
    /usr/three/main.go:15
main.main
    /usr/three/main.go:19
runtime.main
...*/

// 如果不需要增加额外上下文信息,仅附加调用栈后返回,可以使用 WithStack 方法
func foo() error {
    return errors.WithStack(sql.ErrNoRows)
}

ps: 无论是 Wrap, WithMessage 还是 WithStack ,当传入的 err 参数为 nil 时, 都会返回 nil, 这意味着我们在调用此方法之前无需作 nil 判断,保持了代码简洁