Golang_09: Go语言 错误处理(error)、链式/树状error、panic与recover

文章详细介绍了Go语言中错误处理的方式,包括error接口、只有一种错误的情况、多种错误的处理、创建错误的函数如errors.New()和fmt.Errorf(),以及错误处理机制。此外,还讲解了Go1.13之后引入的链式错误(wrapError)和树状错误(wrapErrors),以及如何通过errors.Is()和errors.As()进行错误判断和提取。最后,讨论了panic和recover在错误处理中的角色。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原文链接:https://xiets.blog.csdn.net/article/details/130864936

版权声明:原创文章禁止转载

专栏目录:Golang 专栏(总目录)

Go 调用可能发生异常的函数时,通过多返回一个附加结果作为错误值,习惯上将错误值作为最后一个返回结果。如果返回的错误值为错误类型的零值(如 nil),表示没有发生错误。

1. error 接口

Go 的内置类型中定义了一个 error 接口,用于表示一个错误的通用类型。error 接口的原型:

type error interface {
    Error() string
}

error 接口有一个方法 Error() string,用于返回错误的描述信息。需要错误处理的函数的返回值将包含一个 error 对象,如果 error 是 nil 表示成功(无错误),非 nil 表示失败(有错误)。

2. 只有一种错误的情况

如果错误只有一种情况,一般使用 bool 类型作为错误值类型。例如查询一个 key 在 map 中是否存在:

map1 := make(map[string]string)
...
value, ok := map1[key]
if !ok {
    // map1[key] 不存在
}

3. 有多种错误的情况

更多时候,尤其是 I/O 操作,错误原因可能多种多样,需要给调用者返回一些详细的错误信息。

一般当函数返回一个非 nil 的错误时,它的其他返回值是不明确的而且应该忽略。但有一些函数在出错的情况下可能会返回部分有用的结果(具体看函数的注释文档),例如读取一个文件时发生错误,Read() 函数返回能够成功读取的字节数和对应的错误值。

错误处理示例:

package main

import (
    "bytes"
    "fmt"
    "io"
    "os"
)

func main() {
    // 打开文件
    f, err := os.Open("demo.txt")

    // 处理打开时的错误
    if err != nil {
        // 可以判断错误的类型
        if os.IsNotExist(err) {
            fmt.Printf("文件不能存在: %v\n", err)
        } else {
            fmt.Printf("其他错误: %v\n", err)
        }
        return
    }

    // 函数结束时关闭文件
    defer f.Close()

    // 手动读取文件
    buf := bytes.NewBuffer(nil)
    bs := make([]byte, 32)
    for {
        n, err := f.Read(bs)
        if err != nil {
            if err == io.EOF {
                fmt.Printf("到达文件末尾: %v\n", err)
            } else {
                fmt.Printf("其他错误: %v\n", err)
            }
            if n > 0 {
                // 可能部分成功的情况
                buf.Write(bs[:n])
            }
            break
        }
        buf.Write(bs[:n])
    }

    // 输出读取到的文件内容
    fmt.Printf("文件内容: %s\n", buf.String())
}

使用 “%v” 或 Print 函数输出 error 类型的变量,将调用 Error() string 方法的返回值作为默认的字符串输出格式。

4. 创建错误: errors.New() 和 fmt.Errorf()

在自定义的函数中,如果需要返回一个错误类型,可以自己创建一个错误类型,并实现 error 接口。也可以调用 errors.New()fmt.Errorf() 函数创建一个实现了 error 接口的简单的错误对象,前者传递一个字符串作为错误信息,后者则是格式化一个字符串作为错误信息。

函数原型:

  • errors.New(text string) error
  • fmt.Errorf(format string, a ...interface{}) error

代码实例:

package main

import (
    "fmt"
)

func main() {
    c, err := division(2, 0)
    if err != nil {
        fmt.Printf("发生错误: %v\n", err)   // 发生错误: 错误: 2 / 0, 除数不能为0
        return
    }
    fmt.Println(c)
}

func division(a, b int) (int, error) {
    if b == 0 {
        err := fmt.Errorf("错误: %d / %d, 除数不能为0", a, b)
        // err := errors.New("除数不能为0")
        return 0, err
    }
    c := a / b
    return c, nil
}

5. 错误处理机制

在自定义的函数中调用函数发生错误,一般需要根据错误的类型进行相应的处理,如果无法处理,可以把错误传递下去(返回给上级调用者):

func ReadFile(file string) ([]byte, error) {
    f, err := os.Open(file)
    if err != nil {
        // 返回错误信息给上级调用者
        return nil, err
    }
    defer f.Close()
    return ioutil.ReadAll(f)
}

自定义函数中调用函数返回错误时,如果需要把更多信息返回给上级调用者,可以创建一个新的错误对象返回:

func ReadFile(file string) ([]byte, error) {
    f, err := os.Open(file)
    if err != nil {
        // 创建一个新的错误对象返回, 同时必须附加上原来的错误信息
        err := fmt.Errorf("open file(%s) error: %v", file, err)
        return nil, err
    }
    defer f.Close()
    return ioutil.ReadAll(f)
}

使用 fmt.Errorf() 函数格式化一条错误信息,并返回新的错误对象。在每个函数内传递错误时,为原始的错误信息不断地添加额外的上下文信息,以此建立一个可读可追溯的错误描述。当错误最终被 main 函数处理时,它就得到了一个从最根本问题到总体问题的清晰因果链。

因为错误信息被频繁地使用 err = fmt.Errorf("call func error desc: %v", err) 串联起来,所以错误信息描述字符串首字母不应该大写并且避免换行。一般地,调用函数 f(x) 发生错误,只需要报告 函数f的行为 和 参数值x,因为它们直接和错误上下文相关。

6. 基础 error

上面介绍 error 是属于 Go 1.13 之前的错误处理方式。Go 1.13 之前,标准库对 error 只做了简单的支持,只有 errors.New()fmt.Errorf() 两种方式构造 error 实例,而这两种方式构造出来的 error 都是只对 error 做了简易实现的 errors.errorString 类型。errors 包中的简易 error 实现:

func New(text string) error {
    return &errorString{text}
}

type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

如果在一个函数中,可能返回多种类型错误,如果使用基础 error 实现,一般有两种方式:

  1. 事先创建全局 error 实例,比如创建一个 NotExistError 全局变量,函数中遇到“不存在”的错误,就返回这个实例,然后上层调用者接收到 error 后再通过 ==NotExistError 全局变量比较来判断是否是“不存在”的错误。这种方式虽然可以区分不同的错误类型,但无法携带上下文信息。
  2. 自定义错误类型实现 error 接口,调用者接收到一个 error 后,通过类型断言的方式判断是否是自定义的错误类型。

上面两种方式,在函数需要多级调用时,如果在每一个函数中都需要携带上下文信息,使用 err = fmt.Errorf("xxx: %v", err) 串联 error,那么最终也会丢失掉原始的 error 对象,仅仅传递了错误的文本描述而已。

7. 链式/树状 error

7.1 链式 wrapError

为了解决原始错误对象丢失问题,Go 1.13 在保持兼容原有 error 机制的前提下,在 fmt 包中提供了新的 wrapError 类型:

type wrapError struct {
    msg string
    err error
}

func (e *wrapError) Error() string {
    return e.msg
}

func (e *wrapError) Unwrap() error {
    return e.err
}

wrapError 是结构体类型,并且实现了 error 接口。结构体中使用 msg 字段来存储当前错误的描述信息,并且还增加一个字段 err 用于存储原始的 error 对象,通过 Unwrap() 方法可以获取出上一个错误对象。

wrapError 实例需要通过 fmt.Errorf() 格式化的方式创建,Go 1.13 新增加了 %w 格式化动词用于匹配 error 对象。如果 fmt.Errorf() 存在 %w,则内部创建 wrapError 对象,并把 %w 匹配到的 error 对象存储到 wrapError.err 属性中,最终返回 wrapError 对象。如果没有 %w,则 fmt.Errorf() 内部还是走原始的 errors.New() 的方式返回 errorString 对象。

wrapError 像链表一样,一个 error 链接了另一个 error,因此也被称为 链式error

%w 只能用在 fmt.Errorf() 函数,并且只能匹配 error 类型。

7.2 树状 wrapErrors

Go 1.13 的 wrapError 内部只能存储一个原始 error,因此一次 fmt.Errorf() 只能 wrap 一个 error,也就是 format 中只能出现一个 %w。当一个函数中如果有多个错误需要同时被 wrap,就需要多次 fmt.Errorf()。Go 1.20 在保持兼容原有 wrapError 机制的前提下,在 fmt 包中又提供了新的 wrapErrors 类型:

type wrapErrors struct {
    msg  string
    errs []error
}

func (e *wrapErrors) Error() string {
    return e.msg
}

func (e *wrapErrors) Unwrap() []error {
    return e.errs
}

当使用 fmt.Errorf() 串联 error 时,如果 format 中没有 %w,则走传统的 errors.New();如果只有 1 个 %w,则封装为 wrapError;如果 %w 的数量大于等于 2,则封装为 wrapErrors,把多个 %w 匹配到的 error 存放到 wrapErrors.errs 切片属性中。

wrapErrors 类型的一个 error 可以包含多个 error,其中包含的 error 如果也有 wrapErrors,就形成了树状结构,因此 wrapErrors 也被称为 树状error

7.3 errors 包

errors 包中封装了许多操作 wrapErrorwrapErrors 的函数:

// 把文本封装为 errorString 类型的简单错误
errors.New(text string) error


// 判断 err 错误对象中是否包含了 target 错误对象 (通过 == 判断对象, 不是判断类型), 兼容以前任意方式实现的 error。
//
// 内部通过递归判断: 
//      如果 err 没有 Unwrap() 方法 (传统的 errorString), 则直接通过 err == target 判断
//      如果 err 有 Unwrap() error 方法的 (wrapError), 则通过解包再递归调用判断, 相当于 Is(err.Unwrap(), target)
//      如果 err 有 Unwrap() []error 方法的 (wrapErrors), 则遍历 []error 中的每一个 error, 再递归调用 Is(error, target) 去判断
// 也就是说, 只要 error 是通过 fmt.Errorf() 包装的, 通过 Is() 方法就能判断出最终包装出来的 err 是否包含原 error。
//
// 判断 err 是否是某个错误, 应该使用 errors.Is() 才能兼容新旧类型的 error 机制, 而不是 ==
//
errors.Is(err, target error) bool


// 把多个 err 封装为 errors.joinError 类型的 error, 相对于 wrapErrors 类型但没有 msg 字段
errors.Join(errs ...error) error


// 如果 err 有 Unwrap() error 方法, 则调用 Unwrap() 并返回返回值, 其他情况均返回 nil。如果是 err 是 wrapErrors 也返回 nil。
errors.Unwrap(err error) error


// 在 err 的树状结构中找到第一个与 target 数据类型匹配的 error, 如果找到, 则将 error 存储到 target 并返回 true。否则, 它返回 false。
// 该函数主要用于在一个多层链式/树状包裹的 error 中, 提取想要的目标类型的 error 对象。内部也是使用不断递归 Unwrap() 去寻找目标类型的 error。
errors.As(err error, target any) bool

7.4 error 代码示例

代码示例一:判断返回的 error 是否包含指定的错误

package main

import (
    "errors"
    "fmt"
    "os"
    "path/filepath"
)

// DirNotExists 定义错误类型的实例
var DirNotExists = errors.New("dir not exists")

func main() {
    err := SaveFile("hello.txt", "hello world")
    if err != nil {
        fmt.Println("err:", err)
        if errors.Is(err, DirNotExists) {
            fmt.Println("是缓存文件夹不存在导致的错误")
        }
    }
}

func SaveFile(name string, content string) error {
    cacheDir, err := GetCacheDir()
    if err != nil {
        // %w包装原错误, 并携带上当前函数中的上下文信息
        err = fmt.Errorf("name=%s: %w", name, err)
        return err
    }
    err = os.WriteFile(filepath.Join(cacheDir, name), []byte(content), 0666)
    return err
}

func GetCacheDir() (string, error) {
    // 演示获取缓存目录失败, 直接返回错误
    return "", DirNotExists
}

// Output:
// err: name=hello.txt: dir not exists
// 是缓存文件夹不存在导致的错误

代码示例二:从包装的 error 中获取出指定类型的错误对象

package main

import (
    "errors"
    "fmt"
    "os"
    "path/filepath"
)

// DirNotExists 定义错误类型
type DirNotExists struct {
    DirPath string
}

func (err DirNotExists) Error() string {
    return "file not exists: " + err.DirPath
}

func main() {
    err := SaveFile("hello.txt", "hello world")
    if err != nil {
        fmt.Println("err:", err)
        dirNotExists := &DirNotExists{}
        if ok := errors.As(err, dirNotExists); ok {
            fmt.Println("缓存文件夹路径:", dirNotExists.DirPath)
        }
    }
}

func SaveFile(name string, content string) error {
    cacheDir, err := GetCacheDir()
    if err != nil {
        // %w包装原错误, 并携带上当前函数中的上下文信息
        err = fmt.Errorf("name=%s: %w", name, err)
        return err
    }
    err = os.WriteFile(filepath.Join(cacheDir, name), []byte(content), 0666)
    return err
}

func GetCacheDir() (string, error) {
    cacheDir := "./cache_dir"
    // 演示获取缓存目录失败, 直接返回错误
    err := DirNotExists{
        DirPath: cacheDir,
    }
    return "", err
}

// Output:
// err: name=hello.txt: file not exists: ./cache_dir
// 缓存文件夹路径: ./cache_dir

8. 宕机(panic) 与 恢复(recover)

Go 没有类似其他语言的 try catch 机制捕获错误。Go 可以通过 defer 语句添加延迟函数来捕获宕机错误。

当 Go 程序发生宕机时,正常的程序执行将被终止,已经通过 defer 添加的所有延迟函数将会执行。如果没有在延迟函数中捕获错误恢复程序,则所有延迟函数执行完后将退出进程。

程序中也可以手动调用内置函数 panic() 发生宕机,该函数可以接收任何值作为参数(通常是 error 类型的值)。发生宕机后,后面的程序代码将不再执行。在延迟函数中可以调用 recover() 方法获取 painc() 发布的宕机错误值。当一个宕机错误被延迟函数 recover() 接收返回后,程序将被恢复(进程不会退出),函数将正常返回。当宕机被恢复后,函数返回值如果没有明确赋值,则返回其类型的零值。如果一个函数中发生宕机错误没有被捕获(程序没有被恢复),将把宕机错误传递给上级调用者,直到传递到 main() 函数中仍然没有捕获宕机错误,程序进程将退出。

如果没有发生宕机错误 或 已有的宕机错误已被捕获,此时调用 recover() 方法将返回 nil

宕机示例:

package main

import (
    "fmt"
)

func main() {
    fmt.Println("main() start")

    // 如果 hello() 函数发生了宕机且程序没有恢复, 则宕机错误将传递到当前函数中,
    // 相当于此处发生了宕机, 后面的代码将不再执行。如果在当前函数中继续不捕获宕机错误恢复程序, 
    // 将继续抛给上级调用者, 直到在 main() 函数中依然没有被捕获, 进程将(异常)退出。
    hello()

    fmt.Println("main() end")
}

func hello() {
    fmt.Println("hello() start")
    a := 0
    a = 1 / a       // 除数不能为0, 这里将发生宕机, 后面的语句将不再执行, 然后执行延迟函数
    fmt.Println("hello() end")
}

// 结果输出:
// main() start
// hello() start

宕机与恢复示例:

package main

import (
    "fmt"
)

func main() {
    fmt.Println("main() start")
    hello()
    fmt.Println("main() end")
}

func hello() {
    fmt.Println("hello() start")
    defer func() {
        err := recover()            // 捕获 hello() 函数中发生的宕机错误, 恢复程序
        fmt.Printf("call hello() err: %v\n", err)
        // 宕机错误已被捕获, 程序已被恢复, 对于调用者来说 hello() 函数将正常返回
    }()
    a := 0
    a = 1 / a   // 除数不能为0, 这里将发生宕机, 后面的语句将不再执行, 然后执行延迟函数
    fmt.Println("hello() end")
}

// 结果输出:
// main() start
// hello() start
// call hello() err: runtime error: integer divide by zero
// main() end

手动发生宕机:

package main

import (
    "fmt"
)

func main() {
    fmt.Println("main() start")
    hello()
    fmt.Println("main() end")
}

func hello() {
    defer func() {
        err := recover()        // 捕获宕机错误, 恢复程序
        fmt.Printf("error: %v\n", err)
    }()
    c, r := division(5, 0)
    fmt.Println(c, r)
}

// division a 除以 b, 返回 商 和 余数
func division(a, b int) (int, int) {
    if b == 0 {
        err := fmt.Errorf("%d / %d 错误, 除数不能为0", a, b)
        panic(err)      // 发生宕机, 手动抛出宕机错误给上级调用者, 后面的代码将不再执行
    }
    c := a / b
    r := a % b
    return c, r
}

// 结果输出:
// main() start
// error: 5 / 0 错误, 除数不能为0
// main() end
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谢TS

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值