【新人系列】Golang 入门(十):错误处理详解 - 上

发布于:2025-04-02 ⋅ 阅读:(21) ⋅ 点赞:(0)

✍ 个人博客:https://blog.csdn.net/Newin2020?type=blog
📝 专栏地址:https://blog.csdn.net/newin2020/category_12898955.html
📣 专栏定位:为 0 基础刚入门 Golang 的小伙伴提供详细的讲解,也欢迎大佬们一起交流~
📚 专栏简介:在这个专栏,我将带着大家从 0 开始入门 Golang 的学习。在这个 Golang 的新人系列专栏下,将会总结 Golang 入门基础的一些知识点,并由浅入深的学习这些知识点,方便大家快速入门学习~
❤️ 如果有收获的话,欢迎点赞 👍 收藏 📁 关注,您的支持就是我创作的最大动力 💪

1. 快速了解

1.1 error

go 语言错误处理的理念:开发函数的人需要有一个返回值去告诉调用者是否成功,go 设计者要求我们必须要处理这个 error,代码中会大量出现 if err != nil,保证程序的安全性。

import (
    "errors"
}

func A() (int, error) {
    return 0, errors.New("this is an error")
}

1.2 panic

panic 用于主动抛出一个运行时恐慌,这会导致程序的执行立即停止,并开始展开调用栈,执行所有被延迟(defer)的函数,直到遇到 recover 。

panic 会导致程序的退出,平时开发中不要随便使用,它通常用于表示不可恢复的错误情况,比如无法获取必要的资源、违反了内部的不可违背的逻辑等。

//只会打印panic的信息,不会打印最后一句话
func A() {
    panic("this is an panic")
    fmt.Println("this is a func")
}

panic 发生的常见场景:

  • 主动调用 panic 函数
  • 空指针
  • 访问越界的数组元素
  • Map 未初始化 / Map 并发访问
  • 类型断言错误、被除数为 0 等

1.3 recover

有些时候,我们不是主动调用 panic 而是被动调用,导致程序崩溃,而 recover 就可以用于捕获并恢复由 panic 引发的运行时恐慌,它只能在被 defer 的函数内部使用。

当在 defer 函数中调用 recover 时,如果当前的 goroutine 正在经历恐慌,recover 会停止恐慌的展开,并返回传递给 panic 的值。如果当前 goroutine 没有处于恐慌状态,recover 会返回 nil 。

//会打印recover if A: assignment to entry in nil map
func A() {
    defer func() {
        //recover可以获取panic,并打印指定内容,而不是直接打印错误栈
        if r := recover(); r != nil {
            fmt.Println("recover if A: ",r)
        }
    } ()
    //没有初始化map而使用,会报出panic
    var names map[string]string
    names["go"] = "go工程师"
}

注意

  1. defer 需要放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才会生效。
  2. recover 处理异常后,逻辑并不会恢复到 panic 的那个点去。
  3. 多个 defer 会形成栈,后定义的 defer 会先执行。

2. panic

我们从前面的 defer 部分可以知道,当前执行的 goroutine 中有一个 defer 链表的头指针,但其实它也会有一个 panic 链表头指针,panic 链表链起来的是一个个的 _panic 结构体。

panic 链表和 defer 链表类似,也是在链表头上插入新的 _panic 结构体,所以链表头上的 panic 就是当前正在执行的那一个。

在这里插入图片描述

来看个例子,下面这里的函数 A 注册了两个 defer 函数 A1 和 A2 后发生了 panic,执行完两个 defer 注册后,defer 链表中已经注册了 A1 和 A2 函数。

func A() {
    defer A1()
    defer A2()
    // ......
    panic("panicA")
    // code to do something
}

然后就发生了 panic,并且 panic 之后的代码不会再执行了,而是进入了 panic 的处理逻辑。首先会在 panic 链表中增加一项,我们将它记作 panicA,它就是我们当前执行的 panic。

在这里插入图片描述

接着就该执行 defer 链表了,即从头开始执行,不过这里与函数正常流程执行 defer 有些不同,我们回顾一下 _defer 结构体中的内容。

type _defer struct {
    siz     int32     // 参数和返回值共占多少字节,这段空间会直接分配在_defer结构体后面,用于在注册时保存参数,并在执行时拷贝到调用者参数与返回值空间
    started bool      // 标记defer是否已经执行
    sp      uintptr   // 记录注册这个defer的函数栈指针(调用者栈指针),函数可以通过它判断自己注册的defer是否已经执行完了
    pc      uintptr   // deferproc的返回地址
    fn      *funcval  // 注册的function value函数
    _panic  *_panic
    link    *_defer   // 链接到前一个注册的defer结构体
}

panic 执行 defer 时,会先将其 started 置为 true,即标记它已经开始执行了。并且会把 _panic 字段指向当前执行的 panic,标识这个 defer 是由这个 panic 触发的。

在这里插入图片描述

回到例子中,A2 执行前也要先标记,如果函数 A2 能正常结束,则这一项就会被移除,继续执行下一个 defer。之所以这样设计是为了应对 defer 函数没有正常结束的情况。

在这里插入图片描述

例如接下来要执行的 defer 函数 A1 中再次发生了 panic。

func A1() {
    // ......
    panic("panicA1")
    // ......
}
func A() {
    defer A1()
    defer A2()
    // ......
    panic("panicA")
    // code to do something
}

而在执行前,同样会先标记 A1 的 started 和 _panic 字段。

在这里插入图片描述

当 A1 执行到它自己的 panic 时,其后面的代码也不会执行了,会在在 panic 链表头插入一个新的 panic,记为 panicA1,而它就成为当前执行的 panic 了。

在这里插入图片描述

然后同样去执行 defer 链表,但是发现 A1 已经执行了,并且触发它执行的不是当前的 panicA1,而是 panicA。因此根据 A1 这里记录的 panic 指针,找到对应的 panicA,并把它标记为已终止。

这时候我们就可以来看一下 _panic 结构体长啥样。

type _panic struct {
    argp        unsafe.Pointer    // 存储当前要执行的defer的函数参数地址
    arg         interface{}       // panic的参数
    link        *_panic           //链接到之前发生的panic
    recovered   bool              //标记panic是否被恢复
    aborted     bool              //标记panic会否被终止
}

所以回到上面的案例,panicA 会被标记为终止,而 defer A1 这一项也要被移除,现在的 defer 链表就为空了。

在这里插入图片描述

接下来就该打印 panic 信息了,而 panic 打印异常信息时会从链表尾部开始,即按照 panic 发生的顺序逐个输出。因此这里会先输出 panicA,其次是 panicA1。

在这里插入图片描述

没有 recover 发生时,panic 的处理逻辑就像如上这样。这里的关键点有两个:

  • panic 执行 defer 函数的方式:是先标记,后释放,目的是为了终止之前发生的 panic
  • 异常信息的输出方式:所有在 panic 链表上的项都会被输出,输出顺序与 panic 发生的顺序一致