0%

Errors Handle in Go

定义

error 类型是Go内置的错误类型, 其定义为:

1
2
3
type error interface {
Error() string
}

可以发现:

  1. error是一个接口类型, nil通常表示为无错误;
  2. 接口内置方法有且仅有一个: Error() string.

这种简单的接口类型设计使得errorGo中的使用与处理时很灵活的.

创建方式

Go中定义了诸多方式, 进行错误的创建与操作, 这里列举几种:

fmt

通过fmt.Errorf方法可以获得一个error类型变量. 例如上面例子中的fmt.Errorf("Something wrong happend"). 其源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package fmt

import "errors"

// Errorf 通过 format 格式化标识符获得格式化的错误输出信息;
// 如果format标识符中包含有一个`%w`, 并在后面的操作符中是一个错误类型的变量, 那么Errorf方法会返回一个实现了Unwrap方法的嵌套错误;
// 如果format标识符中包含有过个`%w`, 或者后面的操作符中不是是错误类型的变量, 那么Errorf方法不会返回嵌套错误. 此时的 `%w` == `%v`;
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
}

type wrapError struct {
msg string
err error
}

func (e *wrapError) Error() string {
return e.msg
}

func (e *wrapError) Unwrap() error {
return e.err
}

也就是说对于fmt.Errorf方法可以简单分为两类:

  • fmt.Errorf("format string with wrapped error: %w", err): 获得一个嵌套错误, 它包含了原始错误和新的上下文信息。
  • fmt.Errorf("format string with variable: %v, v): 获得一个普通错误.

errors

errors包是Go内置包, 其提供了错误的基本使用方法:New, Wrap, Is, As...;

通过errors.New()创建一个错误;

1
2
3
4
5
import "errors"

func main() {
errors.New("Create by build in errors pkg")
}

github.com/pkg/errors

github.com/pkg/errors

github.com/pkg/errors包是Go的第三方包, 其对错误处理的进行了一定的扩展, 最大的特点是: 增加了错误信息的上下文信息.

通过errors.New()创建一个错误;

1
2
3
4
5
import "github.com/pkg/errors"

func main() {
errors.New("Create by third-part in errors pkg")
}

user define

用户可以在自行实现New方法, 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
type customError struct {
msg string
}

func (c customError) Error() string {
return c.msg
}

func NewCustomError() customError {
return customError{
msg: "This is Qingzhi error",
}
}

使用场景

Blank identifier :_

_Go语言中充当匿名占位符的作用, 其使用方式与普通标识符并无差异. _占位符的作用是提供了一种忽略机制, 避免编译器报错; 例如可以在导包时, 通过_作为包名别称, 即使在本文件代码中未使用此包, 编译器也不会报错; 可以在变量赋值时, 通过_接收,表示选择忽略掉此变量的处理逻辑, 编译器不会告警;

error赋值给_字符, 可以忽略掉对于此错误的处理, 继续下面的逻辑, 例如:

1
2
3
4
5
6
7
func iterate(a, b int) (int, error) {
// ...
return 0, nil
}

result, _ := iterate(x, y)
fmt.Printf("Result is: %v \n", result)

注意: 这并非推荐的写法, 很不安全. 在项目中通常会要求, 必须处理所有的error;

Handler Error With Return Values

代码中最常见的处理方式就是子函数返回错误类型, 父函数进行对应的处理, 例如:

1
2
3
4
5
6
7
8
9
10
11
12
func child() (int, error) {
return 0, fmt.Errorf("Something wrong happend")
}

func parent() {
// ...
result, err := child()
if err != nil {
// Handle error
}
// ...
}

上面的child函数中返回了error作为错误信息, 当其值不为nil时说明需要进行适当的错误处理; 例如可以进行重试操作或者进行数据清理操作等; child函数中通过fmt包内的Errorf方法创建了一个错误;, 并嵌入错误信息Something wrong happend.

Defer, panic, recover

Defer, panic, recover 机制是Go语言独有的异常处理机制, 其类似与其他语言中的try, catch机制.一般用于无法修复的错误. 而error也会在此机制中使用, 下面将一步步的简单介绍Defer, panic, recover, 最后会理解error的使用场景;

  • Defer

    DeferGo中的一个关键字, 其作用是将当前的函数调用放到调用栈里面,而不是立即执行. 在主函数执行完毕后, 会按照defer的逆序, 依次执行调用栈里面的函数. 注意panic也是一种执行完毕状态, 依旧会触发defer函数执行;例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package main

    import (
    "fmt"
    )

    func A() {
    defer fmt.Println("Keep calm!")
    B()
    }
    func B() {
    defer fmt.Println("Else...")
    C()
    }
    func C() {
    defer fmt.Println("Turn on the air conditioner...")
    D()
    }
    func D() {
    defer fmt.Println("If it's more than 30 degrees...")
    }
    func main() {
    A()
    }

    输出为:

    1
    2
    3
    4
    If it's more than 30 degrees...
    Turn on the air conditioner...
    Else...
    Keep calm!

    main中调用了A函数, 其存在多层defer嵌套, 通过输出可以发现, 其输出顺序与defer顺序逆序, 这也是调用栈的本身特点;

  • Panic

    panicGo中的关键字, 其表示出现了无法修复的错误以至于需要对外暴露出来; 例如在上面的示例中, 我们在最内层的函数D中触发panic:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    package main

    import (
    "errors"
    "fmt"
    )

    func A() {
    defer fmt.Println("Keep calm!")
    B()
    }
    func B() {
    defer fmt.Println("Else...")
    C()
    }
    func C() {
    defer fmt.Println("Turn on the air conditioner...")
    D()
    }
    func D() {
    defer fmt.Println("If it's more than 30 degrees...")
    panic(errors.New("Oh .... Global Warming!!!"))
    }
    func main() {
    A()
    }

    输出为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    If it's more than 30 degrees...
    Turn on the air conditioner...
    Else...
    Keep calm!
    panic: Oh .... Global Warming!!!

    goroutine 1 [running]:
    main.D()
    /home/self/cmd/main.go:22 +0xbc
    main.C()
    /home/self/cmd/main.go:18 +0x85
    main.B()
    /home/self/cmd/main.go:14 +0x85
    main.A()
    /home/self/cmd/main.go:10 +0x85
    main.main()
    /home/self/cmd/main.go:25 +0x25
    exit status 2

    可以看到:

    1. panic触发时, defer调用栈内的函数依旧会执行, 随后执行panic;
    2. panic触发时, 会将堆栈信息输出;
  • Recover

    recover用于处理panic场景, 其可以返回panic调用时的传值. recover必须在defer函数中执行, 否则不会生效; 例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    package main

    import (
    "errors"
    "fmt"
    )

    func A() {
    defer fmt.Println("Keep calm!")
    defer func() {
    if x := recover(); x != nil {
    fmt.Printf("Catch panic: %+v \n", x)
    fmt.Printf("Catch panic type: %T \n", x)
    }
    }()
    B()
    }
    func B() {
    defer fmt.Println("Else...")
    C()
    }
    func C() {
    defer fmt.Println("Turn on the air conditioner...")
    D()
    }
    func D() {
    defer fmt.Println("If it's more than 30 degrees...")
    panic(errors.New("Oh .... Global Warming!!!"))
    }
    func main() {
    A()
    }

    输出为:

    1
    2
    3
    4
    5
    6
    If it's more than 30 degrees...
    Turn on the air conditioner...
    Else...
    Catch panic: Oh .... Global Warming!!!
    Catch panic type: *errors.errorString
    Keep calm!

    发现通过recover函数捕获了panic传值Oh .... Global Warming!!!; 并且发现recover捕获后获得变量类型为*errors.errorString类型, 其实现了error接口定义;

    那么我们就可以通过err变量接收panic信息, 并传递给外部, 例如改动下A的函数签名为: func () (err error);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    package main

    import (
    "errors"
    "fmt"
    )

    func A() (err error) {
    defer fmt.Println("Keep calm!")
    defer func() {
    if e := recover(); e != nil {
    err = e.(error)
    }
    }()
    B()
    return
    }
    func B() {
    defer fmt.Println("Else...")
    C()
    }
    func C() {
    defer fmt.Println("Turn on the air conditioner...")
    D()
    }
    func D() {
    defer fmt.Println("If it's more than 30 degrees...")
    panic(errors.New("Oh .... Global Warming!!!"))
    }
    func main() {
    err := A()
    fmt.Printf("Got error: %v\n", err)
    }

    输出为:

    1
    2
    3
    4
    5
    If it's more than 30 degrees...
    Turn on the air conditioner...
    Else...
    Keep calm!
    Got error: Oh .... Global Warming!!!

    Error Wrapping

错误包装允许你将一个错误包装在另一个错误里面,同时保留原始的错误信息和堆栈跟踪。这在调试和错误处理中非常有用。例如:

1
2
3
4
5
6
7
8
9
import "github.com/pkg/errors"

func someFunction() error {
err := otherFunction()
if err != nil {
return errors.Wrap(err, "someFunction failed")
}
return nil
}

相关的操作函数有两个: errors.Is errors.As;

errors.Is函数用于检查一个错误值是否等于给定的错误值,或者是否包含给定的错误值。这个函数考虑了错误链中的所有错误,如果链中的任何错误与给定的错误匹配,则返回true

函数签名如下:

1
func Is(err, target error) bool
  • err是你想要检查的错误。
  • target是你想要检查的特定错误实例。

errors.Is会遍历整个错误链,调用Unwrap方法来获取下一个错误,直到链的末端。如果链中的任何错误与target相等(使用==比较),则Is返回true

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var ErrNotFound = errors.New("not found")

func someFunction() error {
// 这里可能会返回一个包装了ErrNotFound的错误
return fmt.Errorf("some context: %w", ErrNotFound)
}

func main() {
err := someFunction()
if errors.Is(err, ErrNotFound) {
fmt.Println("找到了ErrNotFound错误")
} else {
fmt.Println("错误不是ErrNotFound")
}
}

errors.As函数用于检查一个错误值是否为特定的错误类型,如果是,则将错误的值赋给一个指定的错误类型变量。这对于处理具有特定结构的错误非常有用,例如自定义错误类型。

函数签名如下:

1
func As(err error, target interface{}) bool
  • err是你想要检查的错误。
  • target是一个指向你想要检查的错误类型变量的指针。

errors.As会遍历错误链,尝试将每个错误值转换为target类型。如果成功,它会将错误值赋给target,并返回true。如果整个链中没有任何错误可以转换为target类型,它会返回false

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type MyError struct {
Msg string
}

func (e *MyError) Error() string {
return e.Msg
}

func someFunction() error {
// 这里可能会返回一个包装了MyError的错误
return fmt.Errorf("some context: %w", &MyError{"自定义错误"})
}

func main() {
err := someFunction()
var myErr *MyError
if errors.As(err, &myErr) {
fmt.Println("找到了MyError错误:", myErr.Msg)
} else {
fmt.Println("错误不是MyError类型")
}
}

在这个例子中,如果someFunction返回的错误链中包含MyError类型的错误,errors.As会将其赋给myErr变量,并且返回true。如果没有找到MyError类型的错误,则返回false

errors.Iserrors.As都是错误处理的强大工具,它们使得在错误可能被多次包装的情况下,错误的检查和断言变得更加简单和直接。

总结

本文介绍了error定义, 创建方式, 使用场景等内容; 由于error是非常灵活的类型, 各个项目中一般都会各自进行特性的封装. 而本文介绍了通用场景下的使用, 相信认真读到这里, 可以对error的有一个简单的理解;