跳过正文
  1. 文章/
  2. GoLang/
  3. GoLang高级/

3、错误处理与异常机制

·3394 字·7 分钟· loading · loading · ·
GoLang GoLang高级
GradyYoung
作者
GradyYoung
GoLang高级 - 点击查看当前系列文章
§ 3、错误处理与异常机制 「 当前文章 」

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, &notFound) {
        // 处理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时:

  1. 当前函数执行立即停止
  2. 任何defer语句正常执行
  3. 控制权返回给调用者
  4. 过程递归向上,直到程序崩溃或被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
*/
GoLang高级 - 点击查看当前系列文章
§ 3、错误处理与异常机制 「 当前文章 」