文章目录

  • 1.简介
  • 2.注意事项
  • 2.1 defer 函数入参在 defer 时确定
  • 2.2 defer 执行顺序为后进先出
  • 2.3 defer 函数在 return 语句赋值与返回之间执行
  • 2.4 defer 遇上闭包
  • 2.5 defer in the loop
  • 3.使用场景
  • 3.1 释放资源
  • 3.2 跟踪函数执行
  • 3.3 捕获 panic
  • 4.小结
  • 参考文献


1.简介

defer 用于预设一个函数调用,推迟函数的执行。被推迟的函数会在执行 defer 的函数返回之前执行。

package main

import "fmt"

func main() {
	defer fmt.Println("world")
	fmt.Println("hello")
}

运行输出:

hello
world

2.注意事项

2.1 defer 函数入参在 defer 时确定

被推迟函数的实参(如果该函数为方法还包括接收者)在推迟执行时就会求值,而不是在调用执行时才求值。这样不仅无需担心变量在 defer 函数执行前被改变,还意味着可以给 defer 函数传递不同实参。

for i := 0; i < 5; i++ {
	defer fmt.Printf("%d ", i)
}

2.2 defer 执行顺序为后进先出

被推迟的函数按照后进先出(Last In First Out,LIFO)的顺序执行,因此以上代码在函数返回时会打印 4 3 2 1 0。

2.3 defer 函数在 return 语句赋值与返回之间执行

return 语句不是原子操作,而是被拆成了两步。

rval = xxx
ret

而 defer 函数就是在这两条语句之间执行。

rval = xxx
defer_func
ret

所以被 defer 的函数可以读取和修改带名称的返回值。

// 返回值为 2
func c() (i int) {
    defer func() { i++ }()
    return 1
}

2.4 defer 遇上闭包

简单来说,Go 语言中的闭包就是在函数内引用函数体之外的数据。defer 函数引用同一个外部变量时,在执行时该变量的值已经发生变化。

package main

import "fmt"

func main() {
    var whatever [3]struct{}
    for i := range whatever {
        defer func() { fmt.Println(i) }()
    }
}

运行输出:

2
2
2

解决办法有两种,一种是将 i 作为实参传入,另一种是定义一个同名局部变量供 defer 函数引用。

// 将 i 作为实参传入
func main() {
	var whatever [3]struct{}
	for i := range whatever {
		defer func(i int) { fmt.Println(i) }(i)
	}
}

// 定义一个同名局部变量
func main() {
	var whatever [3]struct{}
	for i := range whatever {
		i := i
		defer func() { fmt.Println(i) }()
	}
}

运行输出:

2
1
0

2.5 defer in the loop

尽可能地不要在 for 循环中使用 defer,因为这可能会导致资源泄漏(Possible resource leak, ‘defer’ is called in the ‘for’ loop)。

defer 不是基于代码块的,而是基于函数的。你在循环中分配资源,那么不应该简单地使用 defer,因为释放资源不会尽可能早地发生(在每次迭代结束时),只有在 for 语句之后(所有迭代之后),即所在函数结束时,defer 函数才会被执行。这带来的后果就是,如果迭代次数过多,那么可能导致资源长时间得不到释放,造成泄漏。

// Bad
for rows.Next() {
   fields, err := db.Query(.....)
   if err != nil {
      // ...
   }
   defer fields.Close()

   // do something with `fields`

}

如果有一个类似上面分配资源的代码段,我们应该将其包裹在一个函数中(匿名函数或有名函数)。在该函数中,使用 defer,资源将在不需要时被立即释放。

// 1.将 defer 放在匿名函数中
for rows.Next() {
    func() {
        fields, err := db.Query(...)
        if err != nil {
            // Handle error and return
            return
        }
        defer fields.Close()

        // do something with `fields`
    }()
}

// 2.将 defer 放在有名函数中然后调用之
func foo(r *db.Row) error {
    fields, err := db.Query(...)
    if err != nil {
        return fmt.Errorf("db.Query error: %w", err)
    }
    defer fields.Close()

    // do something with `fields`
    return nil
}

// 调用有名函数
for rows.Next() {
    if err := foo(rs); err != nil {
        // Handle error and return
        return
    }
}

3.使用场景

3.1 释放资源

defer 推迟函数执行的能力显得非比寻常, 是处理一些事情的有效方式,例如无论以何种路径返回,都必须释放资源的函数。 典型的例子就是解锁和关闭文件。

//将文件的内容作为字符串返回。
func Contents(filename string) (string, error) {
	f, err := os.Open(filename)
	if err != nil {
		return "", err
	}
	defer f.Close()  // f.Close 会在函数结束后运行

	var result []byte
	buf := make([]byte, 100)
	for {
		n, err := f.Read(buf[0:])
		result = append(result, buf[0:n]...)
		if err != nil {
			if err == io.EOF {
				break
			}
			return "", err  // 我们在这里返回后,f 就会被关闭
		}
	}
	return string(result), nil // 我们在这里返回后,f 就会被关闭
}

推迟诸如 Close 之类的函数调用有两点好处:
第一,它能确保你不会忘记关闭文件。如果你以后又为该函数添加了新的返回路径时,这种情况往往就会发生;
第二,它意味着“关闭”离“打开”很近,这总比将它放在函数结尾处要清晰明了。

3.2 跟踪函数执行

使用 defer 可以很简单地跟踪函数的执行。我们可以编写两个简单的跟踪例程,如下所示:

func trace(s string)   { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }

// Use them like this:
func a() {
    trace("a")
    defer untrace("a")
    // do something....
}

我们可以更好地利用这样一个事实,即延迟函数的参数在延迟执行时进行计算。可以将取消跟踪例程的参数设置为跟踪例程。

func trace(s string) string {
    fmt.Println("entering:", s)
    return s
}

func un(s string) {
    fmt.Println("leaving:", s)
}

func a() {
    defer un(trace("a"))
    fmt.Println("in a")
}

func b() {
    defer un(trace("b"))
    fmt.Println("in b")
    a()
}

func main() {
    b()
}

运行输出:

entering: b
in b
entering: a
in a
leaving: a
leaving: b

3.3 捕获 panic

对于习惯于使用其他语言进行块级资源管理的程序员来说,defer 可能看起来很奇怪,但它最有趣、最强大的应用恰恰来自这样一个事实:它不是基于块的,而是基于函数的。在捕获 panic 时便见证了这一点。

被 defer 的函数在主调函数结束前执行,这个时机点正好可以捕获主调函数抛出的 panic,因而 defer 的另一个重要用途就是执行 recover。

package main

import (
    "fmt"
)

func main() {
    defer func() {
        if ok := recover(); ok != nil {
            fmt.Println("recover")
        }
    }()
    panic("error")
}

记住 defer 要放在 panic 执行之前。

4.小结

defer 帮助我们延迟执行函数,在使用时一定要注意相关事项,避免踩坑。另外要知晓 defer 的常用场景,灵活正确地使用,切勿滥用。


参考文献

[1] Defer - A Tour of Go [2] Effective Go Defer [3] The Go Programming Language Specification Defer statements [4] 知乎.Golang之轻松化解defer的温柔陷阱 [5] stackoverflow.defer in the loop - what will be better?