GO语言基础篇(十七)- 异常处理&资源管理详解

613 阅读5分钟

这是我参与8月更文挑战的第 17 天,活动详情查看: 8月更文挑战

异常处理&资源管理

资源管理可以理解是,当我们打开一个文件之后,我们需要关闭;连接数据库之后,我们需要释放。这些事情是需要成对出现的,这些成对的出现,不要忘了写最后的关闭语句。但是,出错了之后,程序可能在中间跳出来,此时如何保证打开的连接被关闭掉?这个就是出错处理和资源管理放在一起考虑的原因

defer调用

Go语言是通过defer调用来实现资源管理的

  1. 确保调用在函数结束时发生
func tryDefer()  {
    defer fmt.Println(1)
    fmt.Println(2)
}

func main() {
    tryDefer()
}

输出:2    1
  1. defer列表为先进后出

如果在fmt.Println(2)前边也加一个defer,它会输出 2、1,defer里边相当于有一个栈,是先进后出的

func tryDefer()  {
    defer fmt.Println(1)
    defer fmt.Println(2)
    fmt.Println(3)
    return
    fmt.Println(4)
}

func main() {
    tryDefer()
}

输出:3  2  1

可以发现,就算中间有退出,1、2也是可以正常打出来的

下边是一个实际场景中的例子

func writeFile(filename string)  {
    file, err := os.Create(filename)
    if err != nil {
        panic(err)
    }
    defer file.Close()

    writer := bufio.NewWriter(file) //这种方式写文件比较快,先写到内存中,再刷到文件中
    defer writer.Flush() //从内存刷到文件中

    for i:=0; i < 10; i++ {
        fmt.Fprintln(writer, "number :" + strconv.Itoa(i))
    }
}

func main() {
    writeFile("abcd.txt")
}
  1. 参数在defer语句时计算
func tryDefer()  {
    for i:=0; i< 100; i++ {
        defer fmt.Println(i)
        if i == 30 {
            panic("stop!!!")
        }
    }
}

打印结果:30  29 ...... 3  2  1  0

虽然当程序退出的时候,i=30,但是它不会打印30个30出来,这就是因为i它是在defer语句时计算

defer语句常用于成对的操作,比如打开关闭、连接断开、加锁解锁。defer没有使用次数的限制

错误处理

以下边这段代码来说明

func openFile(filename string)  {
    file, err := os.OpenFile(filename, os.O_EXCL|os.O_CREATE, 0666)
    if err != nil {
        panic(err)
    }
    defer file.Close()

    writer := bufio.NewWriter(file) //这种方式写文件比较快,先写到内存中,再刷到文件中
    defer writer.Flush() //从内存刷到文件中

    for i:=0; i < 10; i++ {
        fmt.Fprintln(writer, "number :" + strconv.Itoa(i))
    }
}

func main() {
    openFile("abcd.txt")
}

上边这段程序执行的时候会抛一个错误,并且程序会终止掉(这个就是panic的作用)

panic: open abcd.txt: file exists

但是直接挂掉非常不友好,所以需要做一些处理。需要看一下返回的err到底是一个什么东西,是否能够更加细化这个错误。小伙伴可以通过自己IDEA,进入到OpenFile中去找到error对应的源文件。可以发现这个error是一个接口类型,然后内部有一个Error方法,返回值是string类型

type error interface {
    Error() string
}

那在上边遇到错误的时候,就可以这样去打印错误

fmt.Println("Error: ", err.Error())

输出:Error:  open abcd.txt: file exists

如果你进去看os.OpenFile()方法的注释,可以看到

If there is an error, it will be of type *PathError.

也就是说,如果出现错误,实际返回的是一个*PathError类型,此时可以获取到错误的每一部分

if err != nil {
    if pathError, ok := err.(*PathError); !ok {
        panic(err)
    } else {
        fmt.Println(pathError.Op, pathError.Path, pathError.err)
    }
}

输出:open    abcd.txt     file exists

我们也可以自己创建err(也可以去实现error接口里边的方法,实现自己的错误处理)

err := errors.New("This is a error")

panic

panic执行之后做的事情

  • 停止当前函数执行
  • 一直向上返回,执行每一层的defer
  • 如果没遇到recover,程序退出

Go语言的类型系统会捕获许多编译时错误,但有些其他的错误(比如数组越界访问或引用空指针)都需要在运行时进行检查。当Go语言运行时检测到这些错误,就会发生宕机

一个典型的宕机发生时,正常的程序执行终止,goroutine中的所有延迟函数会执行,然后程序会异常退出,并留下一条日志消息。异常消息包括宕机的值,这往往代表某种错误消息,每一个goroutine都会在宕机的时候显示一个函数调用的栈跟踪消息。通常可以借助这条日志消息来诊断问题的原因,而不用再一次运行该程序

内置的宕机函数(panic)可以接受任何值作为参数,如果遇到”不可能发生“ 的状况,宕机是最好的处理方式

recover

  • 仅在defer调用中使用(在程序运行的中间,是没办法调用的)
  • 在defer调用中可以调用panic的值
  • 如果无法处理,可重新panic

如果内置的recover函数在延迟函数的内部调用,而且这个包含defer语句的函数发生宕机,recover会终止当前的宕机状态,并返回宕机的值。函数不会从之前宕机的地方继续运行,而是正常返回。如果recover在其它任何情况下运行,则它没有任何效果且返回nil

从同一个包中发生的宕机进行恢复,有助于简化处理复杂和未知的错误,但一般的原则是,你不应该尝试去恢复从另一个包内发生的宕机。公共的API应该直接报告错误,并且你也不应该恢复一个不是由你来维护的代码出现的宕机,比如调用者提供提供的回调函数,因为你不知道这样做是否安全

package main

import "fmt"

func tryRecover() {
    defer func() {
        r := recover()
        if err, ok := r.(error); ok {
            fmt.Println("Error occurred", err)
        } else {
            panic(r)
        }
    }()
    b := 0
    a := 5 / b
    fmt.Println(a)
}

func main() {
    tryRecover()
}

输出:Error occurred runtime error: integer divide by zero