Go的错误处理思路
Go 语言通过内置的错误接口提供了非常简单的错误处理机制。
Go 语言的错误处理采用显式返回错误的方式,而非传统的异常处理机制。这种设计使代码逻辑更清晰,便于开发者在编译时或运行时明确处理错误。
Go 的错误处理主要围绕以下机制展开:
error 接口:标准的错误表示。
- 显式返回值:通过函数的返回值返回错误。
- 自定义错误:可以通过标准库或自定义的方式创建错误。
panic 和 recover:处理不可恢复的严重错误。
error接口
Go 标准库定义了一个 error 接口,表示一个错误的抽象。error 类型是一个接口类型,定义如下
1
2
3
|
type error interface {
Error() string
}
|
- 实现
error 接口:任何实现了 Error() 方法的类型都可以作为错误。
Error() 方法返回一个描述错误的字符串。
创建错误
func Errorf(format string, a ...any) error :
format :格式字符串(类似于 fmt.Sprintf )。
a ...any :格式化参数。
- 返回值:一个实现了
error 接口的对象(即 error 类型)
func New(text string) error
text :错误信息字符串。
- 返回值:一个实现了
error 接口的对象。
1
2
3
4
5
6
7
8
9
10
11
|
package main
import (
"errors"
"fmt"
)
func main() {
err := errors.New("this is an error")
fmt.Println(err) // 输出:this is an error
}
|
显式返回错误
Go 中,错误通常作为函数的返回值返回,开发者需要显式检查并处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package main
import (
"errors"
"fmt"
)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
}
// 输出:Error: division by zero
|
自定义错误
通过定义自定义类型,可以实现 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
|
package main
import (
"fmt"
)
type DivideError struct {
Dividend int
Divisor int
}
func (e *DivideError) Error() string {
return fmt.Sprintf("cannot divide %d by %d", e.Dividend, e.Divisor)
}
func divide(a, b int) (int, error) {
if b == 0 {
return 0, &DivideError{Dividend: a, Divisor: b}
}
return a / b, nil
}
func main() {
_, err := divide(10, 0)
if err != nil {
fmt.Println(err) // 输出:cannot divide 10 by 0
}
}
|
fmt 包提供了对错误的格式化输出支持:
%v :默认格式。
%+v :如果支持,显示详细的错误信息。
%s :作为字符串输出。
错误链的处理
func Is(err, target error) bool :检查某个错误是否是特定错误或由该错误包装而成。
err :实际的错误(可能是包装过的)。
target :希望判断的目标错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
func findItem(id int) error {
return fmt.Errorf("database error: %w", ErrNotFound)
}
func main() {
err := findItem(1)
if errors.Is(err, ErrNotFound) {
fmt.Println("Item not found")
} else {
fmt.Println("Other error:", err)
}
}
|
func As(err error, target any) bool :将错误转换为特定类型以便进一步处理。
err:你要检查的错误(可能是包装过的)。
target :指向具体类型的指针,如 *MyError 或 *os.PathError 。
true 表示找到了该类型的错误,并已赋值给 target 指向的变量。
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
|
package main
import (
"errors"
"fmt"
)
type MyError struct {
Code int
Msg string
}
func (e *MyError) Error() string {
return fmt.Sprintf("Code: %d, Msg: %s", e.Code, e.Msg)
}
func getError() error {
return &MyError{Code: 404, Msg: "Not Found"}
}
func main() {
err := getError()
var myErr *MyError
if errors.As(err, &myErr) {
fmt.Printf("Custom error - Code: %d, Msg: %s\n", myErr.Code, myErr.Msg)
}
}
|
panic和recover
Go 的 panic 用于处理不可恢复的错误,recover 用于从 panic 中恢复。
panic :panic 用于主动抛出一个运行时恐慌,这会导致程序的执行立即停止,并开始展开调用栈,执行所有被延迟(defer)的函数,直到遇到 recover 。panic 会导致程序的退出,平时开发中不要随便使用,它通常用于表示不可恢复的错误情况,比如无法获取必要的资源、违反了内部的不可违背的逻辑等。
1
2
3
4
5
|
//只会打印panic的信息,不会打印最后一句话
func A() {
panic("this is an panic")
fmt.Println("this is a func")
}
|
recover :捕获 panic,用于捕获并恢复由 panic 引发的运行时恐慌,它只能在被 defer 的函数内部使用。当在 defer 函数中调用 recover 时,如果当前的 goroutine 正在经历 panic ,recover 会停止恐慌的展开,并返回传递给 panic 的值。如果当前 goroutine 没有处于恐慌状态,recover 会返回 nil 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package main
import "fmt"
func safeFunction() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
panic("something went wrong")
}
func main() {
fmt.Println("Starting program...")
safeFunction()
fmt.Println("Program continued after panic")
}
// 输出:Starting program...
// Recovered from panic: something went wrong
// Program continued after panic
|
defer 需要放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才会生效。
recover 处理异常后,逻辑并不会恢复到 panic 的那个点去。
- 多个
defer 会形成栈,后定义的 defer 会先执行。
底层原理
当前执行的 goroutine 中有一个 defer 链表的头指针,其实它也会有一个 panic 链表头指针,panic 链表链起来的是一个个的 _panic 结构体。
panic 链表和 defer 链表类似,也是在链表头上插入新的 _panic 结构体,所以链表头上的 panic 就是当前正在执行的那一个。

1
2
3
4
5
6
7
|
type _panic struct {
argp unsafe.Pointer // 存储当前要执行的defer的函数参数地址
arg interface{} // panic的参数
link *_panic //链接到之前发生的panic
recovered bool //标记panic是否被恢复
aborted bool //标记panic会否被终止
}
|
以下面的代码为例
1
2
3
4
5
6
7
|
func A() {
defer A1()
defer A2()
// ......
panic("panicA")
// code to do something
}
|
执行流程如下
- 函数 A 注册了两个
defer 函数 A1 和 A2 后发生了 panic,执行完两个 defer 注册后,defer 链表中已经注册了 A1 和 A2 函数。
- 发生了 panic,并且 panic 之后的代码不会再执行了,而是进入了 panic 的处理逻辑。首先会在 panic 链表中增加一项,我们将它记作
panicA,它就是我们当前执行的 panic 。

- 接着执行
defer 链表了,即从头开始执行。panic 执行 defer 时,会先将其 started 置为 true,即标记它已经开始执行了。并且会把 _panic 字段指向当前执行的 panic ,标识这个 defer 是由这个 panic 触发的。

- 如果函数 A2 能正常结束,则这一项就会被移除,继续执行下一个 defer。
- 当
def 函数中存在 recover 时,此时就会把当前执行的 panicA 置为已恢复,然后 recover 函数的任务就完成了。程序会继续往下执行 Println 语句,并打印 panic 的信息,直到 A2 函数执行结束。
