Go语言入门20:错误与异常

Keywords: #技术 #Golang #Go 入门笔记
Table of Contents

Go 语言入门基础学习笔记之 Go 语言的错误与异常处理

golang

错误处理

Go 语言通过内置的错误接口提供了非常简单的错误处理机制。

error 类型是一个接口类型,这是它的定义:

type error interface {
    Error() string
}

我们可以在编码中通过实现 error 接口类型来生成错误信息。

函数通常在最后的返回值中返回错误信息。使用 errors.New 可返回一个错误信息:

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // 实现
}

在下面的例子中,我们在调用 Sqrt 的时候传递的一个负数,然后就得到了 non-nilerror 对象,将此对象与 nil 比较,结果为 true,所以 fmt.Println (fmt 包在处理 error 时会调用 Error 方法)被调用,以输出错误,请看下面调用的示例代码:

result, err:= Sqrt(-1)

if err != nil {
   fmt.Println(err)
}
package main

import "fmt"
// 定义一个 DivideError 结构体,用于表示除法错误
type DivideError struct {
    dividee int // 被除数
    divider  int // 除数
}
// 实现 `error` 接口的 Error 方法
func (de *DivideError) Error() string {
    // 定义错误信息的格式
    strFormat := `
    Cannot proceed, the divider is zero.
    dividee: %d
    divider: 0
`
    // 返回格式化的错误信息
    return fmt.Sprintf(strFormat, de.dividee)
}
// 定义 `int` 类型除法运算的函数
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
    // 检查除数是否为零
    if varDivider == 0 {
        // 创建 DivideError 实例并填充数据
        dData := DivideError{
            dividee: varDividee,
            divider: varDivider, // 除数为零
        }
        // 调用 Error 方法获取错误信息
        errorMsg = dData.Error()
        return // 返回结果为 0,错误信息
    } else {
        // 正常情况下返回计算结果和空错误信息
        return varDividee / varDivider, ""
    }
}
func main() {
    // 正常情况:进行除法运算
    if result, errorMsg := Divide(100, 10); errorMsg == "" {
        // 如果没有错误,则打印结果
        fmt.Println("100/10 = ", result)
    }
    // 当除数为零的时候会返回错误信息
    if _, errorMsg := Divide(100, 0); errorMsg != "" {
        // 如果有错误,则打印错误信息
        fmt.Println("errorMsg is: ", errorMsg)
    }
}

// 输出结果
100/10 =  10
errorMsg is:  
    Cannot proceed, the divider is zero.
    dividee: 100
    divider: 0

异常处理

在 Go 语言中,panic 是一种用于处理程序中意外情况的机制。当程序遇到严重错误或无法继续执行的情况时,可以使用 panic 来引发一个错误。引发 panic 会导致程序的正常执行流程中断,并开始执行延迟(defer)函数,最终导致程序崩溃。

Golang 没有结构化异常,使用 panic 抛出错误,recover 捕获错误。

异常的使用场景简单描述:Go 中可以抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理。

使用场景

panic 通常用于以下情况:

  1. 不可恢复的错误:当程序遇到无法处理的错误,例如数组越界、空指针解引用等。
  2. 程序内部错误:例如,逻辑错误或不应该发生的情况。

基本用法

你可以使用内置的 panic 函数来引发一个 panic,例如:

package main

import "fmt"

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from:", r)
        }
    }()

    fmt.Println("Starting program...")
    panic("Something went wrong!")
    fmt.Println("This line will not be executed.")
}
  1. defer 和 recover:在上面的例子中,使用 defer 关键字定义了一个恢复函数。当 panic 发生时,recover 函数可以捕获到这个 panic 并防止程序直接崩溃。
  2. panic:调用 panic 会引发一个运行时错误,程序的执行会立即停止,直到找到一个 recover 来处理它。
  3. 程序崩溃:如果没有使用 recover,程序会打印出错误信息并退出。

注意事项

  1. 利用 recover 处理 panic 指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当 panic 时,recover 无法捕获到 panic,无法防止 panic 扩散。
  2. recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
  3. 多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。

在 Go 语言中,panic 通常会在以下几种情况下引发错误:

  1. 数组越界

当尝试访问数组或切片中不存在的索引时,会引发 panic

arr := []int{1, 2, 3}
fmt.Println(arr[3]) // 引发 panic: index out of range
  1. 空指针解引用

尝试解引用一个为 nil 的指针时,会引发 panic

var p *int
fmt.Println(*p) // 引发 panic: dereference of nil pointer
  1. 类型断言失败

在进行类型断言时,如果断言失败,会引发 panic

var i interface{} = "hello"
s := i.(int) // 引发 panic: interface conversion: interface {} is string, not int
  1. 使用 panic 函数

显式调用 panic 函数时,会引发错误。

panic("something went wrong") // 引发 panic
  1. 运行时错误

某些运行时错误,例如死循环或堆栈溢出,也可能导致 panic

  1. 其他严重错误

在程序中遇到不应该发生的情况,例如逻辑错误、非法状态等,也可以使用 panic 手动引发。

示例代码

以下是一个简单的示例,展示了几种引发 panic 的情况:

package main

import "fmt"

func main() {
    // 数组越界
    arr := []int{1, 2, 3}
    fmt.Println(arr[3]) // panic: index out of range

    // 空指针解引用
    var p *int
    fmt.Println(*p) // panic: dereference of nil pointer

    // 类型断言失败
    var i interface{} = "hello"
    s := i.(int) // panic: interface conversion: interface {} is string, not int
}

可以使用 deferrecover 来捕获 panic,防止程序崩溃。

func safeFunc() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from:", r)
        }
    }()
    
    // 可能引发 panic 的代码
    arr := []int{1, 2, 3}
    fmt.Println(arr[3]) // 引发 panic
}

func main() {
    safeFunc()
    fmt.Println("Program continues...")
}

在这个示例中,recover 捕获了 panic,程序不会崩溃,而是继续执行后面的代码。

其他知识点

  1. panic 的工作机制
  • 引发 panic:当 panic 被调用时,Go 运行时系统会开始展开当前调用栈的函数。所有的 defer 语句都会被执行。
  • 展开调用栈:在 panic 发生后,程序会向上返回,执行每个函数的 defer,直到找到一个 recover 或完全退出程序。
  • 最终崩溃:如果没有 recover 捕获到 panic,程序将打印出错误信息并退出,返回状态码1。
  1. 使用 deferrecover
  • defer 声明:你可以在函数中使用 defer 关键字来声明一个延迟执行的函数。这个函数会在返回之前执行,即使函数中发生了 panic
  • recover 函数recover 可以用来捕获 panic 并阻止程序崩溃。它只在被 defer 声明的函数中有效。
  1. panic 的参数
  • panic 可以接受任意类型的参数,通常是一个字符串或一个错误对象。这个参数会被打印出来,提供有关 panic 的上下文信息。
panic("an error occurred")
  1. 何时使用 panic
  • 程序内部错误:如果遇到不应该发生的逻辑错误,可以使用 panic,例如检查到不符合预期的状态。
  • 初始化错误:在初始化过程中,如果遇到无法恢复的错误,可以使用 panic
  • 开发阶段:在开发期间,panic 可以帮助快速发现问题,但在生产代码中应谨慎使用。
  1. panic 与错误处理的对比
  • 错误处理:Go 语言推荐使用返回值来处理可恢复的错误,使用 error 类型来传递错误信息。
  • panic:用于处理不可恢复的错误,通常意味着程序处于不一致状态。
  1. 性能影响
  • 使用 panicrecover 会对性能有一定影响,尤其是在高频调用的情况下。频繁的 panicrecover 会增加运行时开销。
  1. 其他注意事项
  • 嵌套 panic:如果在 defer 的恢复函数中再次调用 panic,将导致程序再次崩溃,而不会执行外层的 recover
  • 并发:在并发程序中,panic 只会影响当前 goroutine。如果一个 goroutine 发生了 panic,其他 goroutine 不会受到影响。

参考课程:

  1. 8小时转职Golang工程师
  2. Go语言教程 | 菜鸟教程