找回密码
 立即注册
首页 业界区 业界 Golang基础笔记十二之defer、panic、error

Golang基础笔记十二之defer、panic、error

背竽 2025-7-16 22:18:52
本文首发于公众号:Hunter后端
原文链接:Golang基础笔记十二之defer、panic、error
本篇笔记介绍一下 Golang 里 defer、panic 和 error 的相关概念和操作,以下是本篇笔记目录:

  • defer
  • panic
  • error
1、defer

defer 语句用于延迟执行一个函数,这个函数会在函数返回前执行,无论是正常返回还是触发 panic 之后。
它常用于确保一些文件、链接等资源无论是正常还是异常的情况下被释放。
如果有多个 deder 会按照后进先出的顺序来执行。
1. 后进先出

我们可以先看一个示例:
  1. func main() {
  2.     defer fmt.Println("最后输出")
  3.     defer fmt.Println("倒数第二个输出")
  4.     defer fmt.Println("第二个输出")
  5.     defer fmt.Println("第一个输出")
  6. }
复制代码
这个函数执行后,输出的结果如下:
  1. 第一个输出
  2. 第二个输出
  3. 倒数第二个输出
  4. 最后输出
复制代码
从这个操作可以看到,它是满足后进先出的顺序的。
2. 函数返回前执行

接下来我们验证一下它会在函数返回前执行:
  1. func main() {
  2.     defer fmt.Println("defer 输出")
  3.     fmt.Println("正常输出")
  4.     fmt.Println("函数的末尾输出")
  5. }
复制代码
其输出内容如下:
  1. 正常输出
  2. 函数的末尾输出
  3. defer 输出
复制代码
可以看到,defer 执行的功能会在函数末尾的操作之后。
3. 异常操作的 defer

接下来我们模拟一下函数中执行异常的情况:
  1. func main() {
  2.     defer fmt.Println("defer 输出")
  3.     a := 1
  4.     b := 0
  5.     fmt.Println(a / b)
  6. }
复制代码
其输出结果如下:
  1. defer 输出
  2. panic: runtime error: integer divide by zero
复制代码
可以看到,虽然在函数中间执行报错了,但是 defer 后的内容仍然被执行了。
因此,defer 操作可以保证一些资源的释放,即便函数在运行中报错。
4. defer 注意事项

1) defer 函数的参数传递

如果 defer 的函数的使用了参数,那么这个参数在当时就被固定了,即便是后面对其进行了修改,也不会改变,比如下面的例子:
  1. func main() {
  2.     i := 1
  3.     defer fmt.Println("defer i: ", i)
  4.     i += 1
  5.     fmt.Println("final i: ", i)
  6. }
复制代码
其输出内容如下:
  1. final i:  2
  2. defer i:  1
复制代码
可以看到,即便 i 的值被修改,因为 defer 先被执行,所以 i 的值已经被复制,后面对 i 进行修改后,也不会对 defer 操作的内容有所修改。
而如果变量是引用类型,在操作上是一样的,传递的都是变量的副本,但是因为引用类型传递的是引用的副本,而其共享底层数据是一样的,所以修改后会反映到结果上,其示例如下:
  1. func main() {
  2.     s := []int{1, 2, 3}
  3.     defer fmt.Println("defer s: ", s)
  4.     s[1] = 100
  5.     fmt.Println("final s: ", s)
  6. }
复制代码
其输出结果为:
  1. final s:  [1 100 3]
  2. defer s:  [1 100 3]
复制代码
2) defer 获取变量最新值

如果我们想要在 defer 逻辑中获取到变量的最新值,我们可以将参数封装到匿名函数内部,延迟到实际执行的时候再求值:
  1. func main() {
  2.     i := 1
  3.     defer func() { fmt.Println("defer i: ", i) }()
  4.     i += 1
  5.     fmt.Println("final i: ", i)
  6. }
复制代码
其输出的内容为:
  1. final i:  2
  2. defer i:  2
复制代码
2、panic

panic 是 Golang 里的一种异常机制,用于处理不可恢复的错误,比如数组越界,空指针引用等。
当发生 panic 时,程序会立即停止当前函数的执行,逐层向上执行 defer 并输出错误信息,下面是一个示例:
  1. func main() {
  2.     defer fmt.Println("defer content")
  3.     fmt.Println("first message")
  4.     var a []int
  5.     fmt.Println(a[0])
  6.     fmt.Println("wont show")
  7. }
复制代码
输出内容如下:
  1. first message
  2. defer content
  3. panic: runtime error: index out of range [0] with length 0
  4. goroutine 1 [running]:
  5. main.main()
  6.         /../main.go:32 +0x8f
  7. exit status 2
复制代码
1. 主动触发和运行时触发

在程序运行过程中,如果发生了我们没有预料到的错误,就会触发 panic,或者当我们在处理中无法继续处理下去,我们也可以选择主动触发 panic。
运行时触发 panic 就比如上面我们介绍的例子,下面介绍一个主动触发 panic 的示例:
  1. func GetRandomState() bool {
  2.     num := rand.Intn(5)
  3.     if num < 3 {
  4.         return true
  5.     } else {
  6.         return false
  7.     }
  8. }
  9. func main() {
  10.     if GetRandomState() {
  11.         panic("random state error")
  12.     }
  13. }
复制代码
这个函数多执行几次,当返回值为 true 时,就会主动触发 panic,并中断程序:
  1. panic: random state error
  2. goroutine 1 [running]:
  3. main.main()
  4.         /../main.go:33 +0x38
  5. exit status 2
复制代码
2. recover 捕获 panic

我们可以使用 recover 用于在 defer 中捕获 panic,使程序恢复正常执行。
注意,这里的恢复正常执行,并不是跳过发生报错的地方接着执行,而是指在调用这个函数之外的地方接着往后执行,从而使整个 Golang 程序不会崩溃。
下面是一个示例:
  1. func Test() {
  2.     defer func() {
  3.         if r := recover(); r != nil {
  4.             fmt.Println("recovered err info: ", r)
  5.         }
  6.     }()
  7.     if GetRandomState() {
  8.         panic("random state error")
  9.     }
  10. }
  11. func main() {
  12.     Test()
  13.     fmt.Println("after Test")
  14. }
复制代码
当程序报错后,recover 可以捕获到 panic 信息,在这里我们将其打印了出来,并且可以看到,调用 Test() 之后的 fmt.Println() 信息被正常打印出来。
如果是在生产环境,我们可以将其写入日志,或者发送邮件等方式通知到对应的负责人,下面是将错误信息打印出来的结果:
  1. recovered err info:  random state error
复制代码
根据这个信息,只有简单一个 panic 输出的信息,如果我们想获取更详细的信息,比如,在哪个函数的多少行出了什么报错,我们可以使用下面的操作:
  1. func main() {
  2.     defer func() {
  3.         if r := recover(); r != nil {
  4.             stackBuffer := make([]byte, 1024)
  5.             stackSize := runtime.Stack(stackBuffer, false)
  6.             fmt.Printf("recovered err info: %s \n", stackBuffer[:stackSize])
  7.         }
  8.     }()
  9.     if GetRandomState() {
  10.         panic("random state error")
  11.     }
  12. }
复制代码
其中输出的具体的报错信息如下:
  1. recovered err info: goroutine 1 [running]:
  2. main.main.func1()
  3.         /../main.go:36 +0x51
  4. panic({0x5a6cf20?, 0x5a855f8?})
  5.         /usr/local/go/src/runtime/panic.go:791 +0x132
  6. main.main()
  7.         /../main.go:43 +0x5f
复制代码
可以看到报错信息可以详细到在 main 函数的第 43 行,根据这个信息我们可以再去溯源错误。
3、error

error 是 Golang 的标准错误接口,用于表示错误信息:
  1. type error interface {
  2.     Error() string
  3. }
复制代码
1. 返回 error 信息

我们一般约定函数返回值的最后一个为 error 类型,我们可以使用 errors.New() 的方式来创建一个 error 数据,下面是一个使用示例:
  1. package main
  2. import (
  3.     "errors"
  4.     "fmt"
  5. )
  6. func DivideFunc(a, b int) (int, error) {
  7.     if b == 0 {
  8.         return 0, errors.New("division by zero")
  9.     }
  10.     return a / b, nil
  11. }
  12. func main() {
  13.     result, err := DivideFunc(10, 0)
  14.     if err != nil {
  15.         fmt.Println("process error:", err)
  16.         return
  17.     }
  18.     fmt.Println("division result: ", result)
  19. }
复制代码
在这里,我们获取函数返回值,第一个为结果,第二个为 error 信息,处理 error 信息的时候,判断 err 是否为 nil,不为 nil 则说明函数在运行中发生了报错,需要处理。
除了 errors.New() 这种形式,我们也可以使用 fmt.Errorf("division by zero") 的方式返回一个 error 数据:
  1. func DivideFunc(a, b int) (int, error) {
  2.     if b == 0 {
  3.         return 0, fmt.Errorf("division by zero")
  4.     }
  5.     return a / b, nil
  6. }
复制代码
2. 自定义 error

我们可以自定一个 error 信息,用于输出我们自己想要输出的信息,比如我们定义一个 error 信息如下:
  1. type DivideError struct {
  2.     Code int
  3.     Msg  string
  4. }
  5. func (e *DivideError) Error() string {
  6.     return fmt.Sprintf("error_code:%d, error_msg:%s", e.Code, e.Msg)
  7. }
复制代码
在这里,我们使用定义的 DivideError 实现了 error 接口的 Error() 方法,所以可以直接作为 error 类型返回,下面是使用的示例:
  1. func DivideFunc(a, b int) (int, error) {
  2.     if b == 0 {
  3.         err := &DivideError{Code: -1, Msg: "division by zero"}
  4.         return 0, err
  5.     }
  6.     return a / b, nil
  7. }
复制代码
3. 检查特定 error 类型

用于判断和检查特定的 error 类型的函数有两个,一个是 errors.Is(),一个是 errors.As()。
errors.Is() 用于判断返回的错误信息是否是某个特定错误实例,而 errors.As() 用于判断错误信息是否是某个特定类型,即进行类型断言。
以下是两个函数的使用示例:
1) errors.Is()
  1. type DivideError struct {
  2.         Code int
  3.         Msg  string
  4. }
  5. func (e *DivideError) Error() string {
  6.     return fmt.Sprintf("error_code:%d, error_msg:%s", e.Code, e.Msg)
  7. }
  8. var divideError = &DivideError{Code: -1, Msg: "division by zero"}
  9. func DivideFunc(a, b int) (int, error) {
  10.     if b == 0 {
  11.         return 0, divideError
  12.     }
  13.     return a / b, nil
  14. }
  15. func main() {
  16.     result, err := DivideFunc(10, 0)
  17.     if err != nil {
  18.         if errors.Is(err, divideError) {
  19.             fmt.Println("divideError: ", err)
  20.         } else {
  21.             fmt.Println("other error info: ", err)
  22.         }
  23.     }
  24.     fmt.Println("division result: ", result)
  25. }
复制代码
在这里,先对 DivideError 进行了一个实例化 divideError,在返回错误信息后,使用 errors.Is() 判断是否是该错误。
2) errors.As()

errors.As() 操作如下:
  1. type DivideError struct {
  2.         Code int
  3.         Msg  string
  4. }
  5. func (e *DivideError) Error() string {
  6.     return fmt.Sprintf("error_code:%d, error_msg:%s", e.Code, e.Msg)
  7. }
  8. var divideError = &DivideError{Code: -1, Msg: "division by zero"}
  9. func DivideFunc(a, b int) (int, error) {
  10.     if b == 0 {
  11.         return 0, divideError
  12.     }
  13.     return a / b, nil
  14. }
  15. func main() {
  16.     result, err := DivideFunc(10, 0)
  17.     if err != nil {
  18.         var divErr *DivideError
  19.         if errors.As(err, &divErr) {
  20.             fmt.Println("divideError: ", err, divErr.Code, divErr.Msg)
  21.         } else {
  22.             fmt.Println("other error info: ", err)
  23.         }
  24.     }
  25.     fmt.Println("division result: ", result)
  26. }
复制代码
在这里,我们定义了一个 divErr 变量,并使用 errors.As() 函数对其进行类型断言,因此,在后面我们我们可以直接通过 divErr 获取到对应的 Code 和 Msg 信息。
以上就是本篇笔记关于 defer、panic 和 error 的相关内容介绍,在实际程序中,我们一般使用 error 来返回一些可预料,可恢复的错误,并在函数调用结束之后捕获到该错误,并决定接下来的程序应该如何操作。
而 panic 则包括主动触发和运行时的被动触发,主动触发为当我们程序运行中遇上一些可能无法继续运行的错误,我们可以选择 panic 并结束程序运行,而运行时的被动触发则是我们无法预料到的一些错误,从而被动中止程序运行。
在触发 panic 后,程序会中止运行,如果我们有一些需要关闭资源的操作,我们可以在开启资源调用的时候就使用 defer 操作来预防程序中止后无法关闭资源的问题,同时我们可以在 defer 中使用 recover 操作来捕获到 panic 信息并输出到日志或采用其他能通知到程序运行者的方式。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册