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

本文首发于公众号:Hunter后端

原文链接:Golang基础笔记十二之defer、panic、error

本篇笔记介绍一下 Golang 里 defer、panic 和 error 的相关概念和操作,以下是本篇笔记目录:

  1. defer
  2. panic
  3. error

1、defer

defer 语句用于延迟执行一个函数,这个函数会在函数返回前执行,无论是正常返回还是触发 panic 之后。

它常用于确保一些文件、链接等资源无论是正常还是异常的情况下被释放。

如果有多个 deder 会按照后进先出的顺序来执行。

1. 后进先出

我们可以先看一个示例:

go 复制代码
func main() {
    defer fmt.Println("最后输出")
    defer fmt.Println("倒数第二个输出")

    defer fmt.Println("第二个输出")
    defer fmt.Println("第一个输出")
}

这个函数执行后,输出的结果如下:

go 复制代码
第一个输出
第二个输出
倒数第二个输出
最后输出

从这个操作可以看到,它是满足后进先出的顺序的。

2. 函数返回前执行

接下来我们验证一下它会在函数返回前执行:

go 复制代码
func main() {
    defer fmt.Println("defer 输出")

    fmt.Println("正常输出")
    fmt.Println("函数的末尾输出")
}

其输出内容如下:

go 复制代码
正常输出
函数的末尾输出
defer 输出

可以看到,defer 执行的功能会在函数末尾的操作之后。

3. 异常操作的 defer

接下来我们模拟一下函数中执行异常的情况:

go 复制代码
func main() {
    defer fmt.Println("defer 输出")
    a := 1
    b := 0
    fmt.Println(a / b)
}

其输出结果如下:

go 复制代码
defer 输出
panic: runtime error: integer divide by zero

可以看到,虽然在函数中间执行报错了,但是 defer 后的内容仍然被执行了。

因此,defer 操作可以保证一些资源的释放,即便函数在运行中报错。

4. defer 注意事项

1) defer 函数的参数传递

如果 defer 的函数的使用了参数,那么这个参数在当时就被固定了,即便是后面对其进行了修改,也不会改变,比如下面的例子:

go 复制代码
func main() {
    i := 1
    defer fmt.Println("defer i: ", i)

    i += 1
    fmt.Println("final i: ", i)
}

其输出内容如下:

go 复制代码
final i:  2
defer i:  1

可以看到,即便 i 的值被修改,因为 defer 先被执行,所以 i 的值已经被复制,后面对 i 进行修改后,也不会对 defer 操作的内容有所修改。

而如果变量是引用类型,在操作上是一样的,传递的都是变量的副本,但是因为引用类型传递的是引用的副本,而其共享底层数据是一样的,所以修改后会反映到结果上,其示例如下:

go 复制代码
func main() {
    s := []int{1, 2, 3}
    defer fmt.Println("defer s: ", s)

    s[1] = 100
    fmt.Println("final s: ", s)
}

其输出结果为:

go 复制代码
final s:  [1 100 3]
defer s:  [1 100 3]
2) defer 获取变量最新值

如果我们想要在 defer 逻辑中获取到变量的最新值,我们可以将参数封装到匿名函数内部,延迟到实际执行的时候再求值:

go 复制代码
func main() {
    i := 1
    defer func() { fmt.Println("defer i: ", i) }()

    i += 1
    fmt.Println("final i: ", i)
}

其输出的内容为:

go 复制代码
final i:  2
defer i:  2

2、panic

panic 是 Golang 里的一种异常机制,用于处理不可恢复的错误,比如数组越界,空指针引用等。

当发生 panic 时,程序会立即停止当前函数的执行,逐层向上执行 defer 并输出错误信息,下面是一个示例:

go 复制代码
func main() {
    defer fmt.Println("defer content")

    fmt.Println("first message")

    var a []int
    fmt.Println(a[0])

    fmt.Println("wont show")
}

输出内容如下:

go 复制代码
first message
defer content
panic: runtime error: index out of range [0] with length 0

goroutine 1 [running]:
main.main()
        /../main.go:32 +0x8f
exit status 2

1. 主动触发和运行时触发

在程序运行过程中,如果发生了我们没有预料到的错误,就会触发 panic,或者当我们在处理中无法继续处理下去,我们也可以选择主动触发 panic。

运行时触发 panic 就比如上面我们介绍的例子,下面介绍一个主动触发 panic 的示例:

go 复制代码
func GetRandomState() bool {
    num := rand.Intn(5)
    if num < 3 {
        return true
    } else {
        return false
    }
}

func main() {
    if GetRandomState() {
        panic("random state error")
    }
}

这个函数多执行几次,当返回值为 true 时,就会主动触发 panic,并中断程序:

go 复制代码
panic: random state error

goroutine 1 [running]:
main.main()
        /../main.go:33 +0x38
exit status 2

2. recover 捕获 panic

我们可以使用 recover 用于在 defer 中捕获 panic,使程序恢复正常执行。

注意,这里的恢复正常执行,并不是跳过发生报错的地方接着执行,而是指在调用这个函数之外的地方接着往后执行,从而使整个 Golang 程序不会崩溃。

下面是一个示例:

go 复制代码
func Test() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered err info: ", r)
        }
    }()

    if GetRandomState() {
        panic("random state error")
    }
}

func main() {
    Test()
    fmt.Println("after Test")
}

当程序报错后,recover 可以捕获到 panic 信息,在这里我们将其打印了出来,并且可以看到,调用 Test() 之后的 fmt.Println() 信息被正常打印出来。

如果是在生产环境,我们可以将其写入日志,或者发送邮件等方式通知到对应的负责人,下面是将错误信息打印出来的结果:

go 复制代码
recovered err info:  random state error

根据这个信息,只有简单一个 panic 输出的信息,如果我们想获取更详细的信息,比如,在哪个函数的多少行出了什么报错,我们可以使用下面的操作:

go 复制代码
func main() {

    defer func() {
        if r := recover(); r != nil {
            stackBuffer := make([]byte, 1024)
            stackSize := runtime.Stack(stackBuffer, false)

            fmt.Printf("recovered err info: %s \n", stackBuffer[:stackSize])
        }
    }()

    if GetRandomState() {
        panic("random state error")
    }
}

其中输出的具体的报错信息如下:

go 复制代码
recovered err info: goroutine 1 [running]:
main.main.func1()
        /../main.go:36 +0x51
panic({0x5a6cf20?, 0x5a855f8?})
        /usr/local/go/src/runtime/panic.go:791 +0x132
main.main()
        /../main.go:43 +0x5f

可以看到报错信息可以详细到在 main 函数的第 43 行,根据这个信息我们可以再去溯源错误。

3、error

error 是 Golang 的标准错误接口,用于表示错误信息:

go 复制代码
type error interface {
    Error() string
}

1. 返回 error 信息

我们一般约定函数返回值的最后一个为 error 类型,我们可以使用 errors.New() 的方式来创建一个 error 数据,下面是一个使用示例:

go 复制代码
package main

import (
    "errors"
    "fmt"
)

func DivideFunc(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

func main() {
    result, err := DivideFunc(10, 0)

    if err != nil {
        fmt.Println("process error:", err)
        return
    }
    fmt.Println("division result: ", result)
}

在这里,我们获取函数返回值,第一个为结果,第二个为 error 信息,处理 error 信息的时候,判断 err 是否为 nil,不为 nil 则说明函数在运行中发生了报错,需要处理。

除了 errors.New() 这种形式,我们也可以使用 fmt.Errorf("division by zero") 的方式返回一个 error 数据:

go 复制代码
func DivideFunc(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

2. 自定义 error

我们可以自定一个 error 信息,用于输出我们自己想要输出的信息,比如我们定义一个 error 信息如下:

go 复制代码
type DivideError struct {
    Code int
    Msg  string
}

func (e *DivideError) Error() string {
    return fmt.Sprintf("error_code:%d, error_msg:%s", e.Code, e.Msg)
}

在这里,我们使用定义的 DivideError 实现了 error 接口的 Error() 方法,所以可以直接作为 error 类型返回,下面是使用的示例:

go 复制代码
func DivideFunc(a, b int) (int, error) {
    if b == 0 {
        err := &DivideError{Code: -1, Msg: "division by zero"}
        return 0, err
    }
    return a / b, nil
}

3. 检查特定 error 类型

用于判断和检查特定的 error 类型的函数有两个,一个是 errors.Is(),一个是 errors.As()

errors.Is() 用于判断返回的错误信息是否是某个特定错误实例,而 errors.As() 用于判断错误信息是否是某个特定类型,即进行类型断言。

以下是两个函数的使用示例:

1) errors.Is()
go 复制代码
type DivideError struct {
	Code int
	Msg  string
}

func (e *DivideError) Error() string {
    return fmt.Sprintf("error_code:%d, error_msg:%s", e.Code, e.Msg)
}

var divideError = &DivideError{Code: -1, Msg: "division by zero"}

func DivideFunc(a, b int) (int, error) {
    if b == 0 {
        return 0, divideError
    }
    return a / b, nil
}

func main() {
    result, err := DivideFunc(10, 0)

    if err != nil {
        if errors.Is(err, divideError) {
            fmt.Println("divideError: ", err)
        } else {
            fmt.Println("other error info: ", err)
        }
    }
    fmt.Println("division result: ", result)
}

在这里,先对 DivideError 进行了一个实例化 divideError,在返回错误信息后,使用 errors.Is() 判断是否是该错误。

2) errors.As()

errors.As() 操作如下:

go 复制代码
type DivideError struct {
	Code int
	Msg  string
}

func (e *DivideError) Error() string {
    return fmt.Sprintf("error_code:%d, error_msg:%s", e.Code, e.Msg)
}

var divideError = &DivideError{Code: -1, Msg: "division by zero"}

func DivideFunc(a, b int) (int, error) {
    if b == 0 {
        return 0, divideError
    }
    return a / b, nil
}

func main() {
    result, err := DivideFunc(10, 0)

    if err != nil {
        var divErr *DivideError
        if errors.As(err, &divErr) {
            fmt.Println("divideError: ", err, divErr.Code, divErr.Msg)
        } else {
            fmt.Println("other error info: ", err)
        }
    }
    fmt.Println("division result: ", result)
}

在这里,我们定义了一个 divErr 变量,并使用 errors.As() 函数对其进行类型断言,因此,在后面我们我们可以直接通过 divErr 获取到对应的 CodeMsg 信息。

以上就是本篇笔记关于 deferpanicerror 的相关内容介绍,在实际程序中,我们一般使用 error 来返回一些可预料,可恢复的错误,并在函数调用结束之后捕获到该错误,并决定接下来的程序应该如何操作。

panic 则包括主动触发和运行时的被动触发,主动触发为当我们程序运行中遇上一些可能无法继续运行的错误,我们可以选择 panic 并结束程序运行,而运行时的被动触发则是我们无法预料到的一些错误,从而被动中止程序运行。

在触发 panic 后,程序会中止运行,如果我们有一些需要关闭资源的操作,我们可以在开启资源调用的时候就使用 defer 操作来预防程序中止后无法关闭资源的问题,同时我们可以在 defer 中使用 recover 操作来捕获到 panic 信息并输出到日志或采用其他能通知到程序运行者的方式。

相关推荐
XHunter3 天前
Golang基础笔记十一之日期与时间处理
golang基础笔记
XHunter8 天前
Golang基础笔记十之goroutine和channel
golang基础笔记
XHunter10 天前
Golang基础笔记九之方法与接口
golang基础笔记
XHunter1 个月前
Golang基础笔记三之数组和切片
golang基础笔记
XHunter1 个月前
Golang基础笔记二之字符串及其操作
golang基础笔记