Golang 之 defer,panic 和 recover 的用法

一、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()函数执行之后")	//这行及以下的代码不会再执行
}
  • 输出结果:
    在这里插入图片描述
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值