简单理解golang中的defer、panic和recover

代码测试环境

写在前面

本文主要对Golang中的Defer、Panic、Revocer的用法进行了简单描述。

Defer

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
}

首先看一下上面用来复制文件的代码,主要分四个步骤:1)打开源文件,2)创建一个目标文件;3)把源文件的内容复制到目标文件;4)释放源文件和目标文件的资源。

上面的代码存在一个明显的资源泄露问题,比如dst, err := os.Create(dstName)如果出现问题,那么src.Close()将不能运行,资源便得不到释放。

如果有了defer,上面的问题就能很容易地解决了,比如下面的代码。通过使用Defer语句,能很优雅地把打开和释放资源的语句放在一起写,开发者不需再为释放资源而花费太多精力。

func CopyFile(dstName, srcName string) (written int64, err error) {
	src, err := os.Open(srcName)
	if err != nil {
		return
	}
	// 打开一个文件立马就可以调用defer来关闭文件
	defer src.Close()

	dst, err := os.Create(dstName)
	if err != nil {
		return
	}
	// 打开一个文件立马就可以调用defer来关闭文件
	defer dst.Close()

	return io.Copy(dst, src)
}

使用Defer的几条规则

Defer使用起来很方便,不过有几条规则还是要注意:

  1. 调用defer时,被defer的函数的传入参数会在defer语句那里运算

比如下面的代码,函数返回后会输出“0”:

func a() {
	i := 0
	defer fmt.Println(i)
	i++
	return
}
  1. 在一个函数里可以多次调用defer语句,被defer的函数以后进先出的方式运行

比如下面的函数会输出“3210”:

func b() {
	for i := 0; i < 4; i++ {
		defer fmt.Print(i)
	}
}
  1. 在defer语法中调用的函数可以读取、赋值主函数被命名的返回值

如下面的函数,函数c会返回2:

func c() (i int) {
    defer func() { i++ }()
    return 1
}

通过查看golang的源码(比如encoding/json),defer的这个特性主要从来修改函数返回值中的error类型值。

Panic和Recover

Panic能够阻断正常的控制流,抛出异常,它是一个内建的函数。假如函数F调用了Panic,F会立即停止运行,立马运行Defer定义的函数,运行完以后F就返回给调用者了。异常理论上会一直向上传递,直到程序崩溃。

Recover是一个可以恢复控制流的内建函数,需要注意的是它只能在Defer函数内调用。假如函数没有抛出异常,Recover返回nil,否则,它能捕获Panic传过来的值,同时恢复控制流。

下面的例子描述了panic和defer的例子:

package main

import "fmt"

func main() {
	f()
	fmt.Println("Returned normally from f.")
}

func f() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Recovered in f", r)
		}
	}()
	fmt.Println("Calling g.")
	g(0)
	fmt.Println("Returned normally from g.")
}

func g(i int) {
	if i > 3 {
		fmt.Println("Panicking!")
		panic(fmt.Sprintf("%v", i))
	}
	defer fmt.Println("Defer in g", i)
	fmt.Println("Printing in g", i)
	g(i + 1)
}

输出为:

Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.

上面的例子可以说明几个问题:

  1. defer的语句在函数的最后执行,比如g函数的Defer in g逆序输出现象;
  2. Panic异常会依次向调用链上游传递,比如在g函数抛出的异常(传递值为4)在f函数捕获到了;
  3. 如果异常被recover捕获,则不会再继续向上传递。比如未输出Returned normally from g.但是看到了Returned normally from f.,因为在f函数中调用g(0)时发生了异常;而在main函数调用f()函数时异常已经被f函数捕获,异常已经不存在了。

参考