这是我参与8月更文挑战的第 17 天,活动详情查看: 8月更文挑战
异常处理&资源管理
资源管理可以理解是,当我们打开一个文件之后,我们需要关闭;连接数据库之后,我们需要释放。这些事情是需要成对出现的,这些成对的出现,不要忘了写最后的关闭语句。但是,出错了之后,程序可能在中间跳出来,此时如何保证打开的连接被关闭掉?这个就是出错处理和资源管理放在一起考虑的原因
defer调用
Go语言是通过defer调用来实现资源管理的
- 确保调用在函数结束时发生
func tryDefer() {
defer fmt.Println(1)
fmt.Println(2)
}
func main() {
tryDefer()
}
输出:2 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")
}
- 参数在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