Go 的错误处理哲学是"简单、显式、可控"。每个可能出错的函数都可以返回一个错误值,调用方必须显式处理。没有异常、没有隐式传播,一切都在明面上。
error 接口:一个值、一个方法
Go 的 error 接口极简:只有一个 Error() string 方法。
1
2
3
| type error interface {
Error() string
}
|
这意味着任何实现了 Error() string 方法的类型都是错误。Go 标准库提供了许多内置错误类型,最常见的是 errors.New 和 fmt.Errorf。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| package main
import (
"errors"
"fmt"
)
func main() {
// 创建一个基础错误
err := errors.New("文件不存在")
fmt.Println(err) // 输出:文件不存在
// 用 fmt.Errorf 格式化错误信息
path := "/data/config.yaml"
err2 := fmt.Errorf("配置文件 %s 读取失败", path)
fmt.Println(err2) // 输出:配置文件 /data/config.yaml 读取失败
}
|
错误本质上就是一个值。你可以返回它、比较它、存储它,甚至把它传到其他函数。
错误包装与展开
在大型程序中,错误链非常重要。Go 1.13 引入了错误包装,让你在不丢失原始错误的情况下添加上下文信息。
fmt.Errorf 与 %w
fmt.Errorf 的 %w 动词会包装原始错误,而不是替换它。
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 readFile() error {
return errors.New("文件打开失败")
}
func processConfig() error {
err := readFile()
// 用 %w 包装原始错误
return fmt.Errorf("无法读取配置: %w", err)
}
func main() {
err := processConfig()
if err != nil {
fmt.Println(err) // 输出:无法读取配置: 文件打开失败
}
}
|
errors.Is:判断错误类型
errors.Is 会沿着错误链检查是否有匹配的错误。
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 (
"errors"
"fmt"
)
var (
ErrNotFound = errors.New("资源未找到")
)
func findUser(id int) error {
if id <= 0 {
return ErrNotFound
}
return nil
}
func main() {
err := findUser(-1)
if errors.Is(err, ErrNotFound) {
fmt.Println("处理未找到的错误")
}
}
|
errors.As:提取自定义错误类型
如果你的错误是自定义类型,errors.As 可以提取出来。
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
| package main
import (
"errors"
"fmt"
)
// 自定义错误类型
type NotFoundError struct {
Resource string
ID int
}
func (e NotFoundError) Error() string {
return fmt.Sprintf("%s %d 未找到", e.Resource, e.ID)
}
func findUser(id int) error {
if id <= 0 {
return NotFoundError{Resource: "用户", ID: id}
}
return nil
}
func main() {
err := findUser(-1)
var notFound NotFoundError
if errors.As(err, ¬Found) {
fmt.Printf("具体信息:%s,ID:%d\n", notFound.Resource, notFound.ID)
}
}
|
errors.Unwrap:展开一层
errors.Unwrap 会移除最外层包装,返回内层错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| package main
import (
"errors"
"fmt"
)
func inner() error {
return errors.New("内部错误")
}
func outer() error {
return fmt.Errorf("外层包装: %w", inner())
}
func main() {
err := outer()
fmt.Println("原始错误:", err) // 外层包装: 内部错误
fmt.Println("展开一层:", errors.Unwrap(err)) // 内部错误
}
|
panic 和 recover:仅用于真正不可恢复的错误
Go 的 panic 和 recover 类似于其他语言的异常机制,但使用场景非常有限:只有当程序遇到真正无法继续的错误(如空指针解引用)时才使用。绝不要用 panic 处理常规错误。
panic:触发致命错误
panic 会中断当前函数执行,开始栈回溯。
1
2
3
4
5
6
7
8
9
10
11
12
13
| package main
import "fmt"
func riskyOperation() {
fmt.Println("开始危险操作")
panic("出现致命错误")
fmt.Println("这行不会执行")
}
func main() {
riskyOperation()
}
|
recover:捕获 panic
recover 必须在 defer 函数中调用,才能捕获 panic。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| package main
import "fmt"
func safelyDo() {
defer func() {
if r := recover(); r != nil {
fmt.Println("捕获到 panic:", r)
}
}()
panic("出错了")
}
func main() {
safelyDo()
fmt.Println("程序继续执行")
}
|
defer 配合 recover 的模式
这是处理 panic 的标准模式:在 defer 中 recover。
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 doWork() (err error) {
defer func() {
if r := recover(); r != nil {
// 将 panic 转换为 error
err = fmt.Errorf("panic 转换为错误: %v", r)
}
}()
// 可能触发 panic 的代码
panic("不可恢复的错误")
return nil
}
func main() {
err := doWork()
if err != nil {
fmt.Println("捕获到错误:", err)
}
}
|
记住:panic/recover 是最后手段。能用 error 处理的就不要用 panic。
errors.Join:合并多个错误(Go 1.20+)
Go 1.20 引入了 errors.Join,可以将多个错误合并为一个。
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
35
36
| package main
import (
"errors"
"fmt"
)
func validateName(name string) error {
if name == "" {
return errors.New("姓名不能为空")
}
return nil
}
func validateAge(age int) error {
if age < 0 || age > 150 {
return errors.New("年龄必须在 0-150 之间")
}
return nil
}
func main() {
var errs []error
if err := validateName(""); err != nil {
errs = append(errs, err)
}
if err := validateAge(-1); err != nil {
errs = append(errs, err)
}
if len(errs) > 0 {
combinedErr := errors.Join(errs...)
fmt.Println("验证失败:", combinedErr)
}
}
|
errors.Join 返回的错误可以用 errors.Is 和 errors.As 检查所有合并的错误。
最佳实践
何时用 error、何时用 panic
- 用 error:文件读取失败、网络请求超时、用户输入无效等可预期的错误。
- 用 panic:空指针解引用、数组越界、类型断言失败等不可恢复的程序错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // 错误示例:用 panic 处理文件读取
func readFile() string {
data, err := os.ReadFile("config.yaml")
if err != nil {
panic(err) // 不要这样做
}
return string(data)
}
// 正确示例:用 error 处理文件读取
func readFile() (string, error) {
data, err := os.ReadFile("config.yaml")
if err != nil {
return "", err
}
return string(data), nil
}
|
错误日志记录
不要在底层函数中记录日志,把错误返回给调用方,由调用方决定是否记录。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // 不要这样做
func process() error {
err := doSomething()
if err != nil {
log.Printf("处理失败:%v", err) // 过早记录日志
return err
}
return nil
}
// 正确做法
func process() error {
return doSomething() // 只返回错误,不记录
}
func main() {
if err := process(); err != nil {
log.Printf("处理失败:%v", err) // 由调用方记录
}
}
|
错误包装的层次
包装错误时要提供有意义的上下文,但不要重复包装。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 不要重复包装
func readFile() error {
return fmt.Errorf("读取文件失败: %w", fmt.Errorf("打开文件失败: %w", errors.New("权限不足")))
}
// 正确做法:每层包装一层上下文
func openFile() error {
return errors.New("权限不足")
}
func readFile() error {
err := openFile()
return fmt.Errorf("读取文件失败: %w", err)
}
|
错误处理的一致性
保持项目中错误处理的一致性:统一使用 errors.New 定义错误变量,统一包装方式。
1
2
3
4
5
6
| // 定义标准错误变量
var (
ErrNotFound = errors.New("资源未找到")
ErrInvalidInput = errors.New("无效输入")
ErrPermission = errors.New("权限不足")
)
|
总结
Go 的错误处理看似简单,但背后蕴含着深刻的设计哲学:显式胜于隐式、简单胜于复杂。多返回值让错误处理变得直观,error 接口提供了足够的灵活性,错误包装让错误链变得清晰,而 panic/recover 则为真正不可恢复的情况提供了最后的保障。
记住:错误是值,不是异常。正确地使用 error,谨慎地使用 panic,你的 Go 程序会更加健壮可靠。