Golang defer 快速上手

Posted 恋喵大鲤鱼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang defer 快速上手相关的知识,希望对你有一定的参考价值。

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?

以上是关于Golang defer 快速上手的主要内容,如果未能解决你的问题,请参考以下文章

Golang logrus 快速上手

Golang logrus 快速上手

Golang zap 快速上手

轻松掌握golang的defer机制

golang学习之defer

golang defer 延后执行什么