Go语言的错误处理思想及设计包含以下特征:
nil
,否则返回错误。Go语言没有类似Java或.NET中的异常处理机制,虽然可以使用 defer、panic、recover 模拟,但官方并不主张这样做,Go语言的设计者认为其他语言的异常机制已被过度使用,上层逻辑需要为函数发生的异常付出太多的资源,同时,如果函数使用者觉得错误处理很麻烦而忽略错误,那么程序将在不可预知的时刻崩溃。
Go语言希望开发者将错误处理视为正常开发必须实现的环节,正确地处理每一个可能发生错误的函数,同时,Go语言使用返回值返回错误的机制,也能大幅降低编译器、运行时处理错误的复杂度,让开发者真正地掌握错误的处理。
Go语言的错误处理遵循几个核心原则:
Go中的所有错误都满足内置的error
接口:
type error interface {
Error() string
}
这个简单的接口只有一个方法,用于返回错误描述字符串。任何实现了Error()
方法的类型都可以作为错误使用。
Go的标准错误处理模式是将错误作为函数的最后一个返回值:
这种模式的关键点是:
nil
,表示没有错误nil
错误func readFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err // 返回错误
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return nil, err
}
return data, nil // 成功时返回nil错误
}
func main() {
data, err := readFile("config.json")
if err != nil {
fmt.Println("读取文件失败:", err)
return
}
// 处理数据...
fmt.Println("文件内容:", string(data))
}
Go提供了多种创建错误的方式:
// 使用errors.New创建简单错误
import "errors"
err1 := errors.New("数据库连接失败")
// 使用fmt.Errorf创建格式化错误
import "fmt"
name := "config.json"
err2 := fmt.Errorf("找不到文件: %s", name)
对于需要携带额外信息的错误,可以创建自定义错误类型:
自定义错误类型的优势:
// 定义自定义错误类型
type QueryError struct {
Query string
Err error
}
// 实现error接口
func (e *QueryError) Error() string {
return fmt.Sprintf("查询 %q 失败: %v", e.Query, e.Err)
}
// 使用自定义错误
func executeQuery(query string) error {
// ...
return &QueryError{
Query: query,
Err: errors.New("数据库连接超时"),
}
}
// 使用类型断言检查具体错误类型
func handleQuery() {
err := executeQuery("SELECT * FROM users")
if err != nil {
if qErr, ok := err.(*QueryError); ok {
fmt.Printf("查询错误: %s, 原始错误: %v\n",
qErr.Query, qErr.Err)
} else {
fmt.Println("其他错误:", err)
}
}
}
Go 1.13引入了错误包装功能,通过fmt.Errorf
结合%w
谓词实现:
错误包装的优势:
package main
import (
"errors"
"fmt"
)
type QueryError struct {
Query string
Err error
}
func (e *QueryError) Error() string {
return fmt.Sprintf("查询 %q 失败: %v", e.Query, e.Err)
}
func main() {
// 包装错误
originalErr := errors.New("数据库连接失败")
wrappedErr := fmt.Errorf("执行查询时出错: %w", originalErr)
// 解包错误
fmt.Println(wrappedErr) // 执行查询时出错: 数据库连接失败
// 使用errors.Unwrap获取原始错误
if errors.Unwrap(wrappedErr) == originalErr {
fmt.Println("原始错误被正确识别")
}
// 使用errors.Is检查错误链中是否包含特定错误
if errors.Is(wrappedErr, originalErr) {
fmt.Println("错误链中包含原始错误")
}
// 使用errors.As获取错误链中的特定类型
var queryErr *QueryError
if errors.As(wrappedErr, &queryErr) {
fmt.Println("找到QueryError类型错误")
}
}
一个好的原则是每个错误只处理一次。处理错误意味着:
// 不推荐的方式
func processFile(filename string) error {
data, err := ioutil.ReadFile(filename)
if err != nil {
log.Printf("读取文件失败: %v", err) // 记录错误
return err // 又返回错误
}
// ...
}
// 推荐的方式
func processFile(filename string) error {
data, err := ioutil.ReadFile(filename)
if err != nil {
return fmt.Errorf("读取文件 %s 失败: %w", filename, err)
}
// ...
}
// 或者如果这是调用链的终点
func handleRequest() {
err := processFile("config.json")
if err != nil {
log.Printf("请求处理失败: %v", err)
http.Error(w, "内部服务器错误", 500)
return
}
// ...
}
预定义特定错误值用于比较:
var (
ErrNotFound = errors.New("资源未找到")
ErrPermission = errors.New("权限不足")
)
func GetResource(id string) (*Resource, error) {
// ...
return nil, ErrNotFound
}
// 使用
res, err := GetResource("123")
if err == ErrNotFound {
// 处理"未找到"情况
}
通过类型断言或errors.As
检查错误类型:
type NotFoundError struct {
Resource string
}
func (e NotFoundError) Error() string {
return fmt.Sprintf("资源 %s 未找到", e.Resource)
}
// 使用
if err != nil {
var notFound NotFoundError
if errors.As(err, ¬Found) {
// 处理NotFoundError
}
}
Go 1.20+新增,检查错误是否实现了特定接口,关注错误能做什么,而非错误是什么:
// 定义行为接口
type Temporary interface {
Temporary() bool
}
// 实现接口的错误
type NetworkError struct {
Msg string
IsTemp bool
}
func (e NetworkError) Error() string {
return e.Msg
}
func (e NetworkError) Temporary() bool {
return e.IsTemp
}
// 基于行为处理错误
func handleConnection() {
for {
err := connect()
if err != nil {
// 检查错误行为
var temp interface{ Temporary() bool }
if errors.As(err, &temp) && temp.Temporary() {
// 临时错误,稍后重试
time.Sleep(time.Second)
continue
}
// 永久错误,停止尝试
log.Fatalf("连接失败: %v", err)
}
break
}
}
Go 1.20引入了errors.Join
函数,用于组合多个错误:
// 组合多个错误
err1 := errors.New("错误1")
err2 := errors.New("错误2")
err3 := errors.New("错误3")
combinedErr := errors.Join(err1, err2, err3)
fmt.Println(combinedErr)
// 检查组合错误中是否包含特定错误
if errors.Is(combinedErr, err2) {
fmt.Println("组合错误包含err2")
}
Go语言的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等,这些运行时错误会引起宕机。
一般而言,当宕机发生时,程序会中断运行,并立即执行在该 goroutine(可以先理解成线程)中被延迟的函数(defer 机制),随后,程序崩溃并输出日志信息,日志信息包括 panic()
的参数以及函数调用的堆栈跟踪信息, panic()
的参数通常是某种错误信息。
虽然Go语言的 panic 机制类似于其他语言的异常,但 panic 的适用场景有一些不同,由于panic 会引起程序的崩溃,因此 panic 一般用于严重错误,如程序内部的逻辑不一致。任何崩溃都表明了我们的代码中可能存在漏洞,所以对于大部分漏洞,我们应该使用Go语言提供的错误机制,而不是 panic。
panic
是Go中的异常机制,用于处理不可恢复的错误:
func divide(a, b int) int {
if b == 0 {
panic("除数不能为零")
}
return a / b
}
当调用panic
时:
recover
捕获recover()
是一个Go语言的内建函数,recover
允许程序捕获panic并恢复正常执行:
recover仅在延迟函数defer中直接调用才有效,在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果,如果当前的 goroutine 陷入panic,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。
panic/recover应该仅用于以下场景:
由于函数ff()
中有recover()
,所以当panic发生时,会执行包含了recover()
的defer语句,然后退出当前函数ff()
,程序继续执行
package main
import "fmt"
func main() {
ff()
fmt.Println("CC")
}
func ff() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("has panic : %v\n", err)
}
}()
fmt.Println("AA")
panic("Test Panic")
fmt.Println("BB")
}
/*
AA
has panic : Test Panic
CC
*/
某些操作可能会引发隐藏的panic,应小心处理:
// 可能引发panic的操作
var users []User
fmt.Println(users[0]) // 索引越界
var m map[string]int
m["key"] = 1 // nil map赋值
var p *Person
fmt.Println(p.Name) // 空指针解引用
// 安全的替代方案
if len(users) > 0 {
fmt.Println(users[0])
}
m := make(map[string]int)
m["key"] = 1
if p != nil {
fmt.Println(p.Name)
}
当 panic()
触发的宕机发生时,panic()
后面的代码将不会被运行
但是在panic()
函数前程序执行到的 defer 语句依然会在panic()
前执行
package main
import "fmt"
func main() {
fmt.Println("start")
defer fmt.Println("AA")
defer fmt.Println("BB")
panic("Test Panic")
defer fmt.Println("CC")
}
/*
start
BB
AA
panic: Test Panic
goroutine 1 [running]:
main.main()
/Users/yanggang/Desktop/helloworld/main.go:9 +0xe0
exit status 2
*/