Go 语言中的类没有构造函数和析构函数的概念,处理错误和异常时也没有提供
try...catch...finally
之类的语法,那当我们想要在某个资源使用完毕后将其释放(网络连接、文件句柄等),或者在代码运行过程中抛出错误时执行一段兜底逻辑,要怎么做呢?
通过defer
关键字声明兜底执行或者释放资源的语句可以轻松解决这个问题
注:defer是一个关键字,ctrl+左键后不会进行包的跳转
如果一条语句干不完清理的工作,也可以在 defer 后加一个匿名函数来执行对应的兜底逻辑:
defer func() {
// 执行复杂的清理工作…
} ()
另外,一个函数/方法中可以存在多个 defer 语句,defer 语句的调用顺序遵循先进后出的原则,即最后一个 defer 语句将最先被执行,相当于「栈」这个数据结构,如果在循环语句中包含了 defer 语句,则对应的 defer 语句执行顺序依然符合先进后出的规则。
由于 defer 语句的执行时机和调用顺序,所以我们要尽量在函数/方法的前面定义它们,以免在后面编写代码时漏掉,尤其是运行时抛出错误会中断后面代码的执行,也就感知不到后面的 defer 语句。
func panic(v interface{})
注:panic函数是一个内置函数,在Builtin.go里面
前面介绍了 Go 语言通过 error 类型统一进行错误处理,但这些错误都是我们在编写代码时就已经预见并返回的,对于某些运行时错误,比如数组越界、除数为0、空指针引用,这些 Go 语言是怎么处理的呢?
Go 语言没有像 Java、PHP 那样引入异常的概念,也没有提供 try…catch 这样的语法对运行时异常进行捕获和处理,当代码运行时出错,而又没有在编码时显式返回错误时,Go 语言会抛出 panic,中文译作「运行时恐慌」,我们也可以将其看作 Go 语言版的异常
panic 函数支持的参数类型是 interface{}:
func panic(v interface{})
所以可以传入任意类型的参数:
panic(500) // 传入数字
panic(errors.New(“除数不能为0”)) // 传入 error 类型
func recover() interface{}
recover函数是一个内置函数,在Builtin.go里面
我们还可以通过 recover() 函数对 panic 进行捕获和处理,从而避免程序崩溃然后直接退出,而是继续可以执行后续代码,实现类似 Java、PHP 中 try…catch 语句的功能。
由于执行到抛出 panic 的问题代码时,会中断后续其他代码的执行,所以,显然这个 panic 的捕获应该放到 defer 语句中完成,才可以在抛出 panic 时通过 recover 函数将其捕获,defer 语句执行完毕后,会退出抛出 panic 的当前函数,回调调用它的地方继续后续代码的执行。
可以类比为 panic、recover、defer 组合起来实现了传统面向对象编程异常处理的 try…catch…finally 功能。
这样一来,当程序运行过程中抛出 panic 时我们可以通过 recover() 函数对其进行捕获和处理,如果没有抛出则什么也不做,从而确保了代码的健壮性。
//比如我们看 Go 内置的 io/ioutil 包提供的读取文件方法 ReadFile 实现源码,其中就有 defer 语句的使用:
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
var n int64 = bytes.MinRead
if fi, err := f.Stat(); err == nil {
if size := fi.Size() + bytes.MinRead; size > n {
n = size
}
}
return readAll(f, n)
}
//底层抛出 panic
package main
import "fmt"
func main() {
var i = 1
var j = 0
var k = i / j
fmt.Printf("%d / %d = %d\n", i, j, k)
}
/*
panic: runtime error: integer divide by zero
goroutine 1 [running]:
main.main()
D:/all project/go/demo/main/1.go:8 +0x11
*/
//在这段代码中,我们定义了两个 defer 语句,并且是在函数最顶部,以确保异常情况下也能执行。
/*在函数正常执行的情况下,这两个 defer 语句会在最后一条打印语句执行完成后先执行第二条 defer 语句,再执行第一条 defer 语句:
*/
package main
import "fmt"
func printError() {
fmt.Println("兜底执行")
}
func main() {
defer printError()
defer func() {
fmt.Println("除数不能是0!")
}()
var i = 1
var j = 1
var k = i / j
fmt.Printf("%d / %d = %d\n", i, j, k)
}
/*输出以下三行:
1 / 1 = 1
除数不能是0!
兜底执行
*/
//底层抛出 panic
//而如果我们把 j 的值设置为 0,则函数会抛出 panic:
/*表示除数不能为零。这个时候,由于 defer 语句定义在抛出 panic 代码的前面,所以依然会被执行,底层的逻辑是在执行 var k = i / j 这条语句时,遇到除数为 0,则抛出 panic,然后立即中断当前函数 main 的执行(后续其他语句都不再执行),并按照先进后出顺序依次执行已经在当前函数中声明过的 defer 语句,最后打印出 panic 日志及错误信息*/
package main
import "fmt"
func printError() {
fmt.Println("兜底执行")
}
func main() {
defer printError()
defer func() {
fmt.Println("除数不能是0!")
}()
var i = 1
var j = 0
var k = i / j
fmt.Printf("%d / %d = %d\n", i, j, k)
}
/*
除数不能是0!
兜底执行
panic: runtime error: integer divide by zero
goroutine 1 [running]:
main.main()
D:/all project/go/demo/main/1.go:20 +0x46
*/
/*没有通过 recover() 函数捕获 panic 的话,程序会直接崩溃退出,并打印错误和堆栈信息,但现在我们在 divide() 函数的 defer 语句中通过 recover() 函数捕获了 panic,并打印捕获到的错误信息,这个时候,程序会退出 divide() 函数而不是整个应用,继续执行 main() 函数中的后续代码,即恢复后续其他代码的执行:
*/
package main
import (
"fmt"
)
func divide() {
defer func() {
fmt.Println("222222222222")
}()
defer func() {
fmt.Println("11111111")
}()
var i = 1
var j = 0
k := i / j
fmt.Println(k)
}
func main() {
defer func() {
fmt.Println("333333333333")
}()
divide()
fmt.Println("divide 方法调用完毕,回到 main 函数")
}
/*
11111111
222222222222
333333333333
panic: runtime error: integer divide by zero
goroutine 1 [running]:
main.divide()
D:/ziliao/goproject/try/main.go:18 +0x46
main.main()
D:/ziliao/goproject/try/main.go:26 +0x3f
*/
recove必须搭配defer只用,因为recover是在程序可能出现异常之后才会去执行的
recover函数用来捕捉异常的信息
/*没有通过 recover() 函数捕获 panic 的话,程序会直接崩溃退出,并打印错误和堆栈信息,但现在我们在 divide() 函数的 defer 语句中通过 recover() 函数捕获了 panic,并打印捕获到的错误信息,这个时候,程序会退出 divide() 函数而不是整个应用,继续执行 main() 函数中的后续代码,即恢复后续其他代码的执行:
*/
package main
import (
"fmt"
)
func divide() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("Runtime panic caught: %v\n", err)
}
}()
var i = 1
var j = 0
k := i / j
fmt.Printf("%d / %d = %d\n", i, j, k)
}
func main() {
divide()
fmt.Println("divide 方法调用完毕,回到 main 函数")
}
/*输出:
Runtime panic caught: runtime error: integer divide by zero
divide 方法调用完毕,回到 main 函数
*/
/*如果在代码执行过程中没有抛出 panic,比如我们把 divide() 函数中的 j 值改为 1,则代码会正常执行到函数末尾,然后调用 defer 语句声明的匿名函数,此时 recover() 函数返回值为 nil,不会执行 if 分支代码,然后退出 divide() 函数回到 main() 函数执行后续代码*/
package main
import (
"fmt"
)
func divide() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("Runtime panic caught: %v\n", err)
}
}()
var i = 1
var j = 1
k := i / j
fmt.Printf("%d / %d = %d\n", i, j, k)
}
func main() {
divide()
fmt.Println("divide 方法调用完毕,回到 main 函数")
}
/*输出:
1 / 1 = 1
divide 方法调用完毕,回到 main 函数
*/
/*没有通过 recover() 函数捕获 panic 的话,程序会直接崩溃退出,并打印错误和堆栈信息,但现在我们在 divide() 函数的 defer 语句中通过 recover() 函数捕获了 panic,并打印捕获到的错误信息,这个时候,程序会退出 divide() 函数而不是整个应用,继续执行 main() 函数中的后续代码,即恢复后续其他代码的执行:
*/
package main
import (
"fmt"
)
func divide() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("Runtime panic caught: %v\n", err)
}
}()
var i = 1
var j = 0
k := i / j
fmt.Printf("%d / %d = %d\n", i, j, k)
fmt.Println("111111111111")
defer func() {
fmt.Println("111111111111")
}()
}
func main() {
divide()
fmt.Println("divide 方法调用完毕,回到 main 函数")
}
/*输出:
Runtime panic caught: runtime error: integer divide by zero
divide 方法调用完毕,回到 main 函数
*/