Go 异常


error 接口

error 基础使用

Go 语言引入了一个关于错误处理的标准模式,即 error 接口,它是 Go 语言内建的接口类型,该接口的定义如下:

type error interface {
    Error() string
}

由于 Go 的函数支持多返回值,所以一般会用 error 作为其中一个返回值,代表该函数执行过程中或者逻辑有出错。

通常我们会使用 errors.New() 或者 fmt.Errorf() 来返回一个 error 对象,但注意,通过这两种方式返回的 error 对象都是不可以进行比较的,因为 errors.New() 返回的其实是一个地址,不能用来做等值判断,而 fmt.Errorf() 的内部其实也是用到了 errors.New() 。

示例:

package main

import (
    "errors"
    "fmt"
)

func div(num1, num2 int) (int, error) {
    if num2 <= 0 {
        // 也可以用 fmt.Errorf()
        return -1, errors.New("num2 is not a positive number")
    }
    return num1 / num2, nil
}

func main() {
    result, err := div(9, 0)
    if err != nil {
        fmt.Println("div err:", err)
    }
    fmt.Println("div result:", result)

    err2 := errors.New("go")
    err3 := errors.New("go")
    // 比较的是地址
    fmt.Println(err2 == err3)  // false
    // 可以通过Error()方法拿到其中的error字符串信息,比较异常信息字符串
    fmt.Println(err2.Error() == err3.Error())  // true

}

自定义 error 对象

Go 语言内置的 error 创建方法只返回了错误信息,但很多时候我们业务上还需要错误码,即 error code ,所以我们还可以自定义 error 对象。

package main

import "fmt"

// 自定义error类
type MyError struct {
    code int
    msg  string
}

// 对象定义完之后,接下来只需要实现error接口的Error方法即可
// 这样,我们就自定义了一个同时带有错误码和错误信息的error对象
func (e MyError) Error() string {
    return fmt.Sprintf("code: %d, msg: %s", e.code, e.msg)
}

// 自定义error对象的创建函数
func NewError(code int, msg string) error {
    return MyError{code: code, msg: msg}
}

// 错误错误码函数
func Code(e error) int {
    if e, ok := e.(MyError); ok {
        return e.code
    }
    return -1
}

// 错误错误信息函数
func Msg(e error) string {
    if e, ok := e.(MyError); ok {
        return e.msg
    }
    return "internal error"
}

func main() {
    err := NewError(100, "test error")
    fmt.Printf("code: %d, msg: %s", Code(err), Msg(err))
}

defer 关键字

defer 基础使用

defer(延迟)是 Go 语言中的一个关键字,主要用在函数或方法前面,作用是用于函数和方法的延迟调用。在语法上,defer 与普通的函数调用没有什么区别,在使用上也非常简单,只需要弄清楚以下几点即可:

  1. 延迟的函数的什么时候被调用?

    1. 函数 return 的时候
    2. 发生 panic 的时候
  2. 延迟调用的语法规则

    1. defer 关键字后面表达式必须是函数或者方法调用
    2. 延迟内容不能被括号括起来

defer 执行顺序:

当一个函数中有多个 defer 的时候,defer 语句的执行顺序是先进后出 LIFO,即先调用的后执行。

package main

import "fmt"

func test1() {
    fmt.Println("test1")
}

func test2() {
    fmt.Println("test2")
}

func test3() {
    fmt.Println("test3")
}

func err(num int) int {
    fmt.Println("test err")
    return 1 / num
}

func main() {
    defer test1()
    defer test2()
    defer err(0)
    defer test3()
}

运行结果:即使函数或某个延迟调用发生错误,这些调用依旧会被执⾏。

test3
test err
test2
test1
panic: runtime error: integer divide by zero
                                                    
goroutine 1 [running]:                              
main.err(0x0, 0x404d5e)                             
        E:/GO_PROJECT/PRACTISE/src/main.go:19 +0x11f
main.main()                                         
        E:/GO_PROJECT/PRACTISE/src/main.go:28 +0x8d 



defer 与匿名函数结合使用

示例1:无入参的匿名函数

func main() {
    a := 10
    b := 20
    defer func() {
        fmt.Println("匿名函数中a=", a)
        fmt.Println("匿名函数中b=", b)
    }()
    a = 100
    b = 200
    fmt.Println("main函数中a=", a)
    fmt.Println("main函数中b=", b)

}

运行结果:

main函数中a= 100
main函数中b= 200
匿名函数中a= 100
匿名函数中b= 200

defer 会延迟函数的执行,虽然立即调用了匿名函数,但是该匿名函数不会执行,等整个 main() 函数结束之前,再去调用执行匿名函数,所以输出结果如上所示

示例2:有入参的匿名函数

func main() {
    a := 10
    b := 20
    defer func(a, b int) {
        fmt.Println("匿名函数中a=", a)
        fmt.Println("匿名函数中b=", b)
    }(10, 20)
    a = 100
    b = 200
    fmt.Println("main函数中a=", a)
    fmt.Println("main函数中b=", b)

}

运行结果:

main函数中a= 100
main函数中b= 200
匿名函数中a= 10 
匿名函数中b= 20 

当执行到匿名函数时,虽然没有立即调用执行匿名函数,但是已经完成了参数的传递


defer 使用场景

defer 关键字一般用于两个场景:资源释放、配合 recover 处理 panic

1)资源释放

通过 defer 延迟调用机制,我们可以简洁优雅地处理资源回收问题,从而避免在复杂的代码逻辑情况下,遗漏相关的资源回收问题。用的比较多的就是类似网络连接、数据库连接、文件句柄等资源释放。

func CopyFile(dstFile, srcFile string) (wr int64, err error) {
    src, err := os.Open(srcFile)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstFile)
    if err != nil {
        return
    }
    defer dst.Close()
    
    wr, err = io.Copy(dst, src)
    return err
}

只要我们正确打开了某个资源,比如 src 和 dst,没返回 err 的情况下,都可以用 defer 延迟调用来关闭资源,这是 Go 语言中非常常见的一种资源关闭方式。

2)配合 recover 处理 panic

defer 另一个常用的地方就是在处理程序 panic 时(Go 语言中用 panic 来抛出异常,用 recover 来捕获异常)。

所以当程序出现异常而我们又需要知道是发生了什么异常时,就可以用 defer 和 recover 来捕获异常。

func main() {
   defer func() {
      if r := recover(); r != nil {
         fmt.Println(r)
      }
   }()
   a := 1
   b := 0
   fmt.Println("result:", a/b)
}

运行结果:

runtime error: integer divide by zero

可以看到,程序并没有输出 result,这是因为我们尝试对一个除数为 0 的数做除法,这是不允许的,所以程序回 panic,但我们用 defer 在程序发生 panic 的时候捕获了这个异常,所以打印出异常信息:runtime error: integer divide by zero。


defer 与 return

在 return 的时候,defer 具体做了什么?又会带来什么结果?这是一个非常值得探讨的问题,通过这个问题可以看出对 Go 语言掌握得扎不扎实。

示例1:

func deferFunc() {
    var num = 1
    defer fmt.Printf("num is %d", num)
    num = 2
    return
}

func main() {
    deferFunc() // 1
}

为什么结果是 1 ?

因为延迟函数 defer fmt.Printf("num is %d", num) 的参数 num 在 defer 语句出现的时候就已经确定 num=1,所以不管后面怎么修改 a 的值,最终调用 defer 函数传递给 defer 函数的参数已经固定是 1 了,不会再变化。


示例2:

func deferFunc() {
    arr := [4]int{1, 2, 3, 4}
    defer PrintArr(&arr)
    arr[0] = 10
    return
}

func PrintArr(arr *[4]int) {
    fmt.Println(arr)
}

func main() {
    deferFunc() // &[10 2 3 4]

}

为什么 arr[0] 是 10 ?

我们知道在 defer 出现的时候,参数已经确定,但由于这里传递的是地址,地址没变,但是地址对应的内容被修改了,所以输出会被修改。


示例3:

func deferFunc() (res int) {
    num := 1
    defer func() {
        res++
    }()
    return num
}

func main() {
    fmt.Println(deferFunc())  // 2

}

为什么结果是 2 ?

这是一个非常经典的例子,要想准确地知道程序的执行结果,需要我们对函数 return 的执行有一个细致的了解。其实函数的 return 并非一个原子操作,return 的过程可以被分解为以下三步:

  1. 设置返回值
  2. 执行 defer 语句
  3. 将结果返回

所以,在本例中,第一步是将 res 的值设置为 num,此时还未执行 defer,num 的值是1,所以 res 被设置为 1;然后再执行 defer 语句将 res+1,最终将 res 返回,所以结果打印出 2 。


示例4:

func deferFunc() int {
    num := 1
    defer func() {
        num++
    }()
    return num
}

func main() {
    fmt.Println(deferFunc()) // 1
}

为什么结果是 1 ?

本例和前面的区别是返回值是匿名的,但同样可以运用上面的思路,假设返回值为 res,运用前面的思路分析,第一步将 res 设置为 num,此时 num=1,所以res=1,第二步执行 defer 将 num+1,但 res 仍是 1,第三步将 res 返回,所以最终结果是 1 。


示例5:

func deferFunc() (res int) {
    num := 1
    defer func() {
        num++
    }()
    return num
}

func main() {
    fmt.Println(deferFunc()) // 1
}

不难分析运行结果还是 1,同样的三步分析法,因为 defer 改变的是 num 的值,而不是改变的 res 的值,所以结果不会变(若 defer 函数里变为 res++,那么结果就会是 2 了)。


总结:当碰到 defer 与 return 时如何确定最终的返回值?

  1. defer 定义的延迟函数的参数在 defer 语句出时就已经确定下来了

  2. return 不是原子级操作,其执行过程是: 设置返回值 —> 执行 defer 语句 —> 将结果返回


recover

recover 基础使用

异常其实就是指程序运行过程中发生了 panic,为了不让程序 core,可以在程序中加入 recover 机制,将异常捕获,打印出异常,这样也方便我们定位错误。

而 Go 语言为我们提供了专用于“拦截”运行时 panic 的内建函数 recover(),它可以使得当前的程序从运行时 panic 的状态中恢复并重新获得流程控制权。

注意:recover 只有在 defer 调用的函数中有效

func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("出现了panic,使用reover获取信息:", err)
        }
    }()
    fmt.Println("before")
    panic("出现panic")
    fmt.Println("after")

}

运行结果:

before
出现了panic,使用reover获取信息: 出现panic

结果分析:

  • 在有了 recover 后,程序不会在 panic 初中断,而是在执行完 panic() 之后,执行 defer recover() 函数,并且当前函数 panic() 后面的代码不会再执行,但是调用该函数的代码会接着执行。
  • 如果我们在 main 函数中未加入 defer func(){...} 时,程序运行到第 8 行就会 panic 掉,而通常在我们的业务程序中对于程序 panic 是不可容忍的,我们需要程序健壮运行,而不是因为一些 panic 导致挂掉又被拉起,所以当发生 panic 的时候我们要让程序能够继续运行,并且获取到发生 panic 的具体错误,就可以用上述方法。

panic 传递

当一个函数发生了 panic 之后,若在当前函数中没有 recover,会一直向外层传递直到主函数,如果迟迟没有 recover 的话,那么程序将终止。如果在过程中遇到了最近的 recover,则将被捕获。

func testPanic1() {
    fmt.Println("testPanic1 before")
    testPanic2()
    fmt.Println("testPanic1 after")
}

func testPanic2() {
    defer func() {
        recover()
    }()
    fmt.Println("testPanic2 before")
    testPanic3()
    fmt.Println("testPanic2 after")
}

func testPanic3() {
    fmt.Println("testPanic3 before")
    panic("testPanic3 panic")
    fmt.Println("testPanic3 after")
}

func main() {
    fmt.Println("程序开始")
    testPanic1()
    fmt.Println("程序结束")
}

调用解析:

  1. 调用链:main--> testPanic1 --> testPanic2 --> testPanic3,但在 testPanic3 中发现了一个 panic,由于 testPanic3 没有 recover,于是向上找,在 testPanic2 中找到了 recover,panic 被捕获了,程序接着运行。
  2. 由于 testPanic3 发生了 panic,所以不再继续运行,函数跳出返回到 testPanic2,testPanic2 中捕获到了 panic,也不会再继续执行,跳出函数 testPanic2,最终到了 testPanic1 接着运行。

结论:

  1. recover() 只能恢复当前函数级或以当前函数为首的调用链中的函数中的panic(),恢复后正调用的当前函数结束,但是调用此函数的函数继续执行。

  2. 函数发生了 panic 之后会一直向上传递,如果直至 main 函数都没有 recover(),程序将终止,如果碰见了 recover(),则会被 recover 捕获。


recover 捕获次数

一个 recover 只能捕获一次 panic ,且一一对应。

func main() {
   defer func() {
      if e := recover(); e != nil {
         fmt.Printf("recover:%v\n", e)
      }
   }()
   panic("panic1")
   panic("panic2")
   fmt.Println("111") // 发生panic,不会打印
}

运行结果:

recover:panic1

可以看到,程序总共两次 panic,但是第二次不会执行,因为 recover 捕获到第一个 panic 之后,该函数就退出了。

posted @ 2023-03-13 00:42  Juno3550  阅读(25)  评论(0编辑  收藏  举报