一、defer
概念
- 在 golang 中,defer 代码块会在函数调用链表中增加一个函数调用,是在函数正常返回(也就是 return)之后添加的一个函数调用。因此,defer 通常用于释放函数内部变量
存在隐患的示例
// 实现文件的复制
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
dst, err := os.Create(dstName)
if err != nil {
return
}
written, err = io.Copy(dst, src)
dst.Close()
src.Close()
return
}
- 上面的代码中,如果调用
dst, err := os.Create(dstName)
失败,则函数会执行return
退出运行,但之前创建的 src(文件句柄) 并没有被释放。 - 如果代码的逻辑复杂或者调用过多时,很容易发生这样的错误,使用 defer 可以避免这种情况的发生,更改后如下:
package main
import (
"io"
"os"
)
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close()
dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close()
return io.Copy(dst, src)
}
使用规则
规则1:在 defer 表达式确定的时候,defer 修饰的函数里的参数就确定了
- 示例
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
// 输出结果:0
- defer 函数会在 return 之后才被调用,但为什么这段代码输出的是 0 而不是 1 呢?
- 因为 defer 定义的函数 fmt.Println(i),这个变量 i 在被 defer 声明时,值就已经确定了。所以说,上面的代码等价于下面的代码:
func a() {
i := 0
//因为i=0,所以此时就明确告诉golang在程序退出时,执行输出0的操作
defer fmt.Println(0)
i++
return
}
- 在 i++ 后再定义一个 defer
func a() {
i := 0
defer fmt.Println(i)
i++
defer fmt.Println(i)
return
}
// 输出结果:
1
0
- 由此可见,defer 输出的值,就是在其定义时变量的值,而不是等到 return 之后调用 defer 时,变量的真正的值。
- 另外,上例中为何先输出1,再输出0,见下面的规则二。
规则2:defer 的执行顺序为先进后出
- 示例
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}
// 输出结果:
3
2
1
0
规则3:defer 可以读取函数命名的返回值
- 当一个函数(下面的
c()
)的返回值 是一个函数(下面的func()
)时,return 语句真正的执行过程应是:保存返回值(若有)—> 执行 defer 函数(若有)—> 执行 return 跳转 - 示例
func c() (i int){
defer func() { i++ }()
return 1
}
// 输出结果:2
- 执行过程:①保存返回值1,此时 i 的值就是1;②执行 defer 代码块,i++,此时 i 的值变为2;③执行 return 跳转,返回 i 的值即为 2
二、defer() 和 recover()
产生背景:这两个函数因何而生?
- 通常的 error 处理
package main
import (
"errors"
"fmt"
)
func a() (err error) {
err = b()
if err != nil {
return
}
err = c()
if err != nil {
return
}
err = errors.New("a内错误")
return
}
func b() (err error) {
err = errors.New("b内错误")
return
}
func c() (err error) {
err = errors.New("c内错误")
return
}
func main() {
err := a()
if err != nil {
fmt.Println(err)
}
}
- a 函数内调用了 b 和 c 函数,每次调用后都要进行
err != nil
的判断,如果有类似更复杂的多层嵌套,错误判断将变得十分麻烦。实际开发中,比如用户注册功能就要判断很多东西:①表单验证是否 ok;②用户是否已经存在;③数据插入是否正确 等等。 - 而使用 panic 和 recover 就可以优化这样繁琐的判断步骤,分别类似于 throws exception 和 try-catch
- 优化后如下:
package main
import (
"log"
)
func a() {
b()
c()
panic("a内错误")
return
}
func b() {
panic("b内错误")
}
func c() (err error) {
panic("c内错误")
}
func main() {
defer func() {
if r := recover(); r != nil {
log.Printf("错误信息为: %v", r)
}
}()
a()
}
规则
- recover 函数若想起作用,必须在 defer 的函数中使用
- 正常函数执行过程中,调用 recover 没有任何作用,只会返回 nil,如:fmt.Println(recover())
- 如果当前的 goroutine 出现了 panic ,那么 recover 将会捕获这个 panic 的值,并且让程序正常执行下去,而不会 crash
执行逻辑
- ①在函数执行过程中的某处若调用了 panic,则函数的正常执行立即终止,并抛出一条错误信息。
- ②在函数中,该 panic 异常之前定义的 defer 语句将按序执行
- ③ goroutine 停止执行
- 示例
package main
import "fmt"
func f() {
fmt.Println("①:执行函数中的异常前")
panic("异常信息")
fmt.Println("②:异常后") // 这行及以下的代码不会再执行
fmt.Println("...")
}
func main() {
fmt.Println("③:main函数开始")
defer func() { // 必须要先声明 defer,否则不能捕获到 panic 异常
fmt.Println("④:进入defer后,recover之前")
if r := recover(); r != nil {
fmt.Println(r) // 这里的 r 其实就是panic传入的内容
}
fmt.Println("⑤:recover之后")
}()
f() // 开始调用f
fmt.Println("⑥:f()函数执行之后") //这行及以下的代码不会再执行
}
- 输出结果: