go 中的 defer 使用及其规则

时间:2025-03-04 08:45:57

文章目录

    • 1 defer 定义
      • 1.1 简单示例
      • 1.2 具体示例
    • 2 defer 常用场景
    • 3 defer 规则
      • 3.1 当申明defer 时,参数就已经解析了
      • 3.2 defer执行顺序为先进后出
      • 3.3 defer可以读取有名返回值

1 defer 定义

defer 英文原意: vi. 推迟;延期;服从 vt. 使推迟;使延期。

defer的思想类似于C++中的析构函数,不过Go语言中“析构”的不是对象,而是函数,defer就是用来添加函数结束时执行的语句。

析构函数(destructor) 与构造函数相反,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统自动执行析构函数。 析构函数往往用来做“清理善后” 的工作(例如在建立对象时用new开辟了一片内存空间,delete会自动调用析构函数后释放内存)。

defer代码块会在函数调用链表中增加一个函数调用。这个函数调用不是普通的函数调用,而是会在函数正常返回,也就是return之后添加一个函数调用。因此,defer通常用来释放函数内部变量。

1.1 简单示例

  • 首先我们现用简单的例子来体验一下defer函数
package main

import (
    "fmt"
)

func main() {
    fmt.Println("Step 1")
    fmt.Println("Step 2")
    defer fmt.Println("Step Final")
    fmt.Println("Step 3")
    fmt.Println("Step 4")
}

输出

Step 1
Step 2
Step 3
Step 4
Step Final

我们将defer ("Step Final") 换行, 其输出结果也如上所示

func main() {
	defer fmt.Println("Step Final")
    fmt.Println("Step 1")
    fmt.Println("Step 2")
    fmt.Println("Step 3")
    fmt.Println("Step 4")
}

输出

Step 1
Step 2
Step 3
Step 4
Step Final
  • 非主函数:
func f() (result int) {

  defer func() {
    result++
  }()
  return 0
}

上面 返回结果是 1,因为defer中添加了一个函数,在函数返回前改变了命名返回值的值。是不是很好用呢。但是,要注意的是,如果我们的defer语句没有执行,那么defer的函数就不会添加,如果把上面的程序改成这样:

func f() (result int) {

  return 0
  defer func() {
    result++
  }()
  return 0
}

上面的函数就返回0了,因为还没来得及添加defer的东西,函数就返回了。

1.2 具体示例

假设我们想要创建一个文件,写入它,然后在我们完成时关闭1
在文件操作的时候,均需要进行关闭文件操作, 所以我们来用 defer 完成关闭文件操作。

package main

import "fmt"
import "os"

func main() {
    f := createFile("/tmp/")
    defer closeFile(f)
    writeFile(f)
}

func createFile(p string) *os.File {
    fmt.Println("creating")
    f, err := os.Create(p)
    if err != nil {
        panic(err)
    }
    return f
}

输出:

creating
writing
closing

2 defer 常用场景

通过defer,我们可以在代码中优雅的关闭/清理代码中所使用的变量。defer作为golang清理变量的特性,有其独有且明确的行为。

defer经常和 panic 以及 recover 一起使用,判断是否有异常,进行收尾操作。

package main
 
import "fmt"
 
func main(){
    defer func(){ // 必须要先声明defer,否则不能捕获到panic异常
        fmt.Println("c")
        if err:=recover();err!=nil{
            fmt.Println(err) // 这里的err其实就是panic传入的内容,55
        }
        fmt.Println("d")
    }()
    f()
}
 
func f(){
    fmt.Println("a")
    panic(55)
    fmt.Println("b")
    fmt.Println("f")
}

输出结果:

a
c
55
d

3 defer 规则

3.1 当申明defer 时,参数就已经解析了

func a() {
	i := 0
	defer fmt.Println(i)
	i++
	return
}

输出

0

上面我们说过,defer函数会在return之后被调用。那么这段函数执行完之后,是不用应该输出1呢?

读者自行编译看一下,结果输出的是0. why?

这是因为虽然我们在defer后面定义的是一个带变量的函数: (i). 但这个变量(i)在defer被声明的时候,就已经确定其确定的值了2

3.2 defer执行顺序为先进后出

当同时定义了多个defer代码块时,golang安装先定义后执行的顺序依次调用defer。

func b() {
	for i := 0; i < 4; i++ {
		defer fmt.Print(i)
	}
}

输出:

3
2
1
0

3.3 defer可以读取有名返回值

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

输出结果是 2. 在开头的时候,我们说过defer是在return调用之后才执行的。 这里需要明确的是defer代码块的作用域仍然在函数之内,结合上面的函数也就是说,defer的作用域仍然在c函数之内。因此defer仍然可以读取c函数内的变量


  1. /defer ↩︎

  2. /articles/10167 ↩︎

相关文章