Golang学习笔记(七)—— 异常处理

发布时间 2024-01-07 18:49:34作者: 昨晚没做梦

异常处理


异常

在Go语言中,异常被定义为实现了 error 接口的类型;error 接口只包含一个方法 Error() ,用于返回错误信息。

  error 除了输出错误外,往往需要输出当时的业务相关信息(错误地址,错误码,错误信息等),举个简单例子:

package main

import (
    "fmt"
)

const (
    Success = iota
    InvalidUser
    WrongPassword
)

var code2msg = map[int]string{
    Success:       "登录成功",
    InvalidUser:   "无效的用户",
    WrongPassword: "密码错误",
}

type LoginError struct {  //自定义错误类型,添加想要输出的错误信息
    code    int
    Message string
}

func NewLoginError(code int) *LoginError {
    return &LoginError{
        code,
        code2msg[code],
    }
}

func (e *LoginError) Error() string {
    return fmt.Sprintf("ERROR DETAIL: error code:[%+v] error message:[%+v]", e.code, e.Message)
}

func (e *LoginError) Code() int {
    return e.code
}

func login(i int) (bool, error) {if i != 0 {
        return false, NewLoginError(i)
    }
    return true, nil
}

func testlogin(i int) {
    _, err := login(i) //err是error接口类型变量

    if err != nil {
        //用switch对不同种类错误进行处理
        switch err.(type) {
        case *LoginError:
            loginerror := err.(*LoginError) //类型断言
            fmt.Println(loginerror.Error())

            //用switch对错误的不同方面进行处理
            switch loginerror.Code() {
            case InvalidUser:
                //...
            case WrongPassword:
                //...
            }
        //其他错误
        default:
            fmt.Println("other error")
        }
    }

    //登录成功操作
}

func main() {
    testlogin(1)
}

 

异常的抛出和捕获

panic() 函数

在Go语言中,异常可以通过调用 panic() 函数来抛出。

关于panic:

  1. 是内置函数
  2. 假如函数F中 panic,会终止其后要执行的代码,如果函数F存在要执行的 defer 函数列表,按照defer的逆序执行;如果函数F的 defer 函数列表中没有 recover 语句,异常将继续向上抛出,直到被捕获或者程序崩溃。
  3. defer 语句必须放在 panic 前定义!

 

recover() 函数

在Go语言中,异常可以通过调用 recover() 函数来捕获。

关于recover:

  1. 是内置函数
  2. recover 只有在 defer 调用的函数中才有效。否则当 panic 时,recover 甚至都不会执行。
  3. recover 可以放在最外层函数,做统一异常处理。

 

例子

package main

import (
    "fmt"
)

type PanicError struct {
    msg string
}

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

func test2() {
    err := PanicError{"panic error!"} //异常信息
    panic(err.Error())

    defer func() { //无效的 defer
        println("test2_defer")
    }()

}

func test1() {
    recover() //捕获不到异常
    test2()
    recover()            //捕获不到异常
    fmt.Println("test1") //被终止了
}

func test() {
    defer func() {
        println("test_defer")
        if err := recover(); err != nil { //捕获异常,上层函数得以正常执行
            println(err.(string)) //最外层函数对异常处理
        }
    }()
    test1()
    fmt.Println("test") //被终止了
}

func A() {
    test()
    fmt.Println("A")
}

func main() {
    fmt.Println("START")
    A()
    fmt.Println("END")
}

//输出结果:
//START
//A
//END //test_defer //panic error!

 

进一步了解

defer

defer的实现过程:

  1.运行到 defer 语句时,生成对应的 _defer 结构体实例,存到栈中,再调用 deferprocStack( *_defer ) 函数,将其加入当前 g 的 defer 链表头

  (1).如果 defer 语句在循环中,就调用 deferproc( fn func() ) 函数,在里面生成对应的 _defer 结构体实例存到堆中,再将其加入当前 goroutine 的 defer 链表头

  2. return 语句给返回值复制后,调用 deferreturn() 函数,不断获取链表头的 _defer 结构体执行,直到链表为空 或 取得的 _defer 结构体不是当前函数调用的

  3.return 语句执行 RET ,返回上层函数。

 

_defer 结构体数据结构如下:

源码文件:src/runtime/runtime2.go    line:1026

type _defer struct {
    heap      bool    //是否存在堆上
    rangefunc bool
    sp        uintptr    //调用者栈指针
    pc        uintptr    //返回地址
    fn        func()    //函数
    link      *_defer    //链表中下一个defer

    head *atomic.Pointer[_defer]
}

 

开放编码优化(open-coded defer)

实现 open-coded defer 需要满足三个条件:

  1. 没有禁用编译器优化
  2. 函数的 defer 关键字不能在循环中执行
  3. 函数的 defer 数量少于或者等于 8 个,函数的 return 个数与 defer 函数个数的乘积小于或者等于 15 个

  查看是否 open-coded defer 可以在终端输入:go build -gcflags="-d defer" main.go

 

panic

数据结构:

源码文件:src/runtime/runtime2.go    line:1047

type _panic struct {
    argp unsafe.Pointer // 指向 defer 调用时参数的指针
    arg  any            // 调用 panic 时传入的参数
    link *_panic        // 指向前一个 _panic

    startPC uintptr
    startSP unsafe.Pointer

    //指向当前运行的defer的栈帧
    sp unsafe.Pointer
    lr uintptr
    fp unsafe.Pointer

    retpc uintptr

    //用于处理 open-code-defer 的额外状态
    deferBitsPtr *uint8
    slotsPtr     unsafe.Pointer

    recovered   bool    //当前 _panic 是否被恢复
    goexit      bool
    deferreturn bool
}

执行过程:

  1.调用 gopanic,以下操作都在这个函数中

  2.创建新的 _panic 并添加到所在 goroutine 的 _panic 链表的最前面

  3.不断从当前 goroutine 的 _defer 中链表获取 _defer ,运行延迟调用函数

  4.调用 fatalpanic 中止整个程序

 

recover

   recover() 函数非常简单:

  1. 取出当前 goroutine 的 _panic 链表最新的一个 _panic,将其 recoverd 字段赋值 true
  2. 若链表为空,则返回 nil
  3. 完成

 

一些小思考

什么情况下会发生 _panic 链表有多个 _panic?

  答:那就是在处理 panic 时,又调用了 panic,这只能在 defer 函数上才能做到。

多个 _panic 该怎么处理?

  答:就像函数嵌套一样,从外层到内层处理