Go语言的错误处理思想及设计包含以下特征:
- 一个可能造成错误的函数,需要返回值中返回一个错误接口(error),如果调用是成功的,错误接口将返回
nil,否则返回错误。 - 在函数调用后需要检查错误,如果发生错误,则进行必要的错误处理。
Go语言没有类似Java或.NET中的异常处理机制,虽然可以使用 defer、panic、recover 模拟,但官方并不主张这样做,Go语言的设计者认为其他语言的异常机制已被过度使用,上层逻辑需要为函数发生的异常付出太多的资源,同时,如果函数使用者觉得错误处理很麻烦而忽略错误,那么程序将在不可预知的时刻崩溃。
Go语言希望开发者将错误处理视为正常开发必须实现的环节,正确地处理每一个可能发生错误的函数,同时,Go语言使用返回值返回错误的机制,也能大幅降低编译器、运行时处理错误的复杂度,让开发者真正地掌握错误的处理。
错误处理基础 #
错误处理哲学 #
Go语言的错误处理遵循几个核心原则:
- 显式优于隐式:错误必须被显式检查和处理
- 简单明了:使用简单的返回值而非复杂的异常机制
- 错误是值:错误在Go中是普通值,可以像其他值一样传递和操作
- 错误处理是正常控制流的一部分:而不是特殊路径
error接口 #
Go中的所有错误都满足内置的error接口:
type error interface {
Error() string
}
这个简单的接口只有一个方法,用于返回错误描述字符串。任何实现了Error()方法的类型都可以作为错误使用。
基本错误处理方式 #
Go的标准错误处理模式是将错误作为函数的最后一个返回值:
这种模式的关键点是:
- 使用多返回值,最后一个返回值为错误
- 调用者必须显式检查错误
- 错误的零值是
nil,表示没有错误 - 正常情况下返回实际结果和
nil错误
package main
import (
"errors"
"fmt"
)
// readFile 读取文件
func readFile(fn string) (string, error) {
if fn == "" {
return "", errors.New("file name is empty")
}
return "file content", nil
}
func main() {
content, err := readFile("")
if err != nil {
// err.Error() 获取错误信息
fmt.Println(err.Error())
return
}
fmt.Println(content)
}
创建和使用错误 #
errors.New #
errors包实现了一个基本的 error,具体实现方式如下
func New(text string) error {
return &errorString{text}
}
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
一般情况下,我们可以使用errors.New来创建一个error
import "errors"
err1 := errors.New("数据库连接失败")
自定义错误类型 #
对于需要携带额外信息的错误,可以创建自定义错误类型:
自定义错误类型的优势:
- 可以携带上下文信息
- 允许调用者通过类型断言获取详细信息
- 可以实现错误分层和包装
package main
import "fmt"
type FileReadError struct {
Msg string
}
func (f *FileReadError) Error() string {
return f.Msg
}
func NewFileReadError(msg string) error {
return &FileReadError{msg}
}
func main() {
err := NewFileReadError("file name is empty")
if err != nil {
if _, ok := err.(*FileReadError); ok {
fmt.Printf("FileReadError %s\n", err.Error())
} else {
fmt.Printf("Other Error %s\n", err.Error())
}
}
}
错误包装 #
在 Go 语言中,错误包装(Error Wrapping) 是一种通过链式结构记录错误上下文的技术,允许在错误传播过程中保留原始错误信息,同时附加额外的调试或日志信息。自 Go 1.13 起,标准库通过 fmt.Errorf 的 %w 动词和 errors.Unwrap/errors.Is/errors.As 等函数原生支持错误包装。
fmt.Errorf("msg %w", err):用%w包裹原始错误err,生成新错误。errors.Unwrap(err):解包错误,返回被包裹的原始错误。errors.Is(err, target):检查错误链中是否存在target错误。errors.As(err, target):将错误链中的错误转换为指定类型。
errors.Unwrap 解包错误 #
package main
import (
"errors"
"fmt"
)
func main() {
err1 := errors.New("error-1")
err2 := fmt.Errorf("error-2 %w", err1)
err3 := fmt.Errorf("error-3 %w", err2)
// 逐层解包
for err3 != nil {
fmt.Println(err3)
err3 = errors.Unwrap(err3)
}
}
/*
error-3 error-2 error-1
error-2 error-1
error-1
*/
errors.Is 检查特定错误 #
使用 errors.Is 判断错误链中是否包含指定错误:
package main
import (
"errors"
"fmt"
)
type FileNotFoundError struct {
Msg string
}
func (e *FileNotFoundError) Error() string {
return e.Msg
}
func NewFileNotFoundError(msg string) error {
return &FileNotFoundError{msg}
}
func main() {
notFoundErr := NewFileNotFoundError("File not found")
readRrr := fmt.Errorf("File Read Error %w", notFoundErr)
fmt.Println(errors.Is(readRrr, notFoundErr)) // true
}
errors.As 类型断言 #
使用 errors.As 将错误转换为具体类型:
package main
import (
"errors"
"fmt"
)
type FileNotFoundError struct {
Msg string
}
func (e *FileNotFoundError) Error() string {
return e.Msg
}
func NewFileNotFoundError(msg string) error {
return &FileNotFoundError{msg}
}
func main() {
notFoundErr := NewFileNotFoundError("File not found")
readRrr := fmt.Errorf("File Read Error %w", notFoundErr)
var nf *FileNotFoundError
if b := errors.As(readRrr, &nf); b {
fmt.Println(nf.Error()) // File not found
}
}
错误处理策略与模式 #
仅处理一次错误 #
一个好的原则是每个错误只处理一次。处理错误意味着:
- 修复错误并继续
- 将错误包装并向上传播
- 记录错误并停止处理
// 不推荐的方式
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
}
// ...
}
错误处理模式 #
哨兵错误(Sentinel Errors) #
预定义特定错误值用于比较:
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")
}
panic与recover机制 #
Go语言的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等,这些运行时错误会引起宕机。
一般而言,当宕机发生时,程序会中断运行,并立即执行在该 goroutine(可以先理解成线程)中被延迟的函数(defer 机制),随后,程序崩溃并输出日志信息,日志信息包括 panic() 的参数以及函数调用的堆栈跟踪信息, panic() 的参数通常是某种错误信息。
虽然Go语言的 panic 机制类似于其他语言的异常,但 panic 的适用场景有一些不同,由于panic 会引起程序的崩溃,因此 panic 一般用于严重错误,如程序内部的逻辑不一致。任何崩溃都表明了我们的代码中可能存在漏洞,所以对于大部分漏洞,我们应该使用Go语言提供的错误机制,而不是 panic。
panic 基础 #
panic是Go中的异常机制,用于处理不可恢复的错误:
func divide(a, b int) int {
if b == 0 {
panic("除数不能为零")
}
return a / b
}
当调用panic时:
- 当前函数执行立即停止
- 任何defer语句正常执行
- 控制权返回给调用者
- 过程递归向上,直到程序崩溃或被
recover捕获
recover捕获panic #
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,应小心处理:
// 可能引发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和defer #
当 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
*/