golang-defer

2023-07-30 16:42:32 浏览数 (2)

defer的使用特点

其实其中一点特性我理解起来就有点像java中的finally的用法

关于官方解释

代码语言:javascript复制
A defer statement defers the execution of a function until the surrounding function returns.

The deferred call's arguments are evaluated immediately, but the function call is not executed until the surrounding function returns.

这里提到了defer调用的参数会立即计算,但在周围函数返回之前不会执行函数调用。

以及延迟函数调用被压入堆栈。当函数返回时,其延迟调用以后进先出顺序执行。

它有如何特点

  • 所在的函数中,它在 return 或 panic 或 执行完毕 后被调用
  • 多个 defer,它们的被调用顺序,为栈的形式。先进后出,先定义的后被调用

看下面几个例子:

  1. 在计算defer语句时,将计算延迟函数的参数。在此示例中,在延迟Println调用时计算表达式“i”。函数返回后,延迟调用将打印“0”。
代码语言:javascript复制
func a() {
    i := 0
    defer fmt.Println(i)
    i  
    return
}
  1. 在周围函数返回后,延迟函数调用以后进先出顺序执行。
代码语言:javascript复制
func b() {
    for i := 0; i < 4; i   {
        defer fmt.Print(i)
    }
}   //将会打印3210

然后不免在使用过程中会遇到这些坑

坑1. defer在匿名返回值和命名返回值函数中的不同表现

代码语言:javascript复制
func returnValues() int {
    var result int
    defer func() {
        result  
        fmt.Println("defer")
    }()
    return result
}

func namedReturnValues() (result int) {
    defer func() {
        result  
        fmt.Println("defer")
    }()
    return result
}

&nbsp;&nbsp;上面的方法会输出0,下面的方法输出1。上面的方法使用了匿名返回值,下面的使用了命名返回值,除此之外其他的逻辑均相同,为什么输出的结果会有区别呢?

&nbsp;&nbsp;要搞清这个问题首先需要了解defer的执行逻辑,defer语句在方法返回“时”触发,也就是说return和defer是“同时”执行的。以匿名返回值方法举例,过程如下。

  • 将result赋值给返回值(可以理解成Go自动创建了一个返回值retValue,相当于执行retValue = result)
  • 然后检查是否有defer,如果有则执行
  • 返回刚才创建的返回值(retValue)

在这种情况下,defer中的修改是对result执行的,而不是retValue,所以defer返回的依然是retValue。在命名返回值方法中,由于返回值在方法定义时已经被定义,所以没有创建retValue的过程,result就是retValue,defer对于result的修改也会被直接返回。

坑2. 判断执行没有err之后,再defer释放资源

一些获取资源的操作可能会返回err参数,我们可以选择忽略返回的err参数,但是如果要使用defer进行延迟释放的的话,需要在使用defer之前先判断是否存在err,如果资源没有获取成功,即没有必要也不应该再对资源执行释放操作。如果不判断获取资源是否成功就执行释放操作的话,还有可能导致释放方法执行错误。

正确做法

代码语言:javascript复制
resp, err := http.Get(url)
// 先判断操作是否成功
if err != nil {
    return err
}
// 如果操作成功,再进行Close操作
defer resp.Body.Close()

坑3. 调用os.Exit时defer不会被执行 当发生panic时,所在goroutine的所有defer会被执行,但是当调用os.Exit()方法退出程序时,defer并不会被执行。

代码语言:javascript复制
func deferExit() {
    defer func() {
        fmt.Println("defer")
    }()
    os.Exit(0)
}

上面的defer并不会输出。

坑4.非引用传参给defer调用的函数,且为非闭包函数,值不会受后面的改变影响

代码语言:javascript复制
func defer0() {
	a := 1  // a 作为演示的参数
	defer fmt.Println(a) // 非引用传参,非闭包函数中,a 的值 不会 受后面的改变影响
	a = a   2
}
// 控制台输出 1

坑5. 传递引用给defer调用的函数,即使不使用闭包函数,值也会受后面的改变影响

代码语言:javascript复制
func myPrintln(point *int)  {
	fmt.Println(*point) // 输出引用所指向的值
}
func defer1() {
	a := 3
	// &a 是 a 的引用。内存中的形式: 0x .... ---> 3
	defer myPrintln(&a) // 传递引用给函数,即使不使用闭包函数,值 会 受后面的改变影响
	a = a   2
}
// 控制台输出 5

坑6. 传递值给defer调用的函数,且非闭包函数,值不会受后面的改变影响

代码语言:javascript复制
func p(a int)  {
	fmt.Println(a)
}

func defer2() {
	a := 3
	defer p(a) // 传递值给函数,且非闭包函数,值 不会 受后面的改变影响
	a = a   2
}
// 控制台输出: 3

坑7. defer调用闭包函数,且内调用外部非传参进来的变量,值会受后面的改变影响

代码语言:javascript复制
// 闭包函数内,事实是该值的引用
func defer3() {
	a := 3
	defer func() {
		fmt.Println(a) // 闭包函数内调用外部非传参进来的变量,事实是该值的引用,值 会 受后面的改变影响
	}()
	a = a   2  // 3   2 = 5
}
// 控制台输出: 5

坑8. defer调用闭包函数,若内部使用了传参参数的值。使用的是值

代码语言:javascript复制
func defer5() {
	a := []int{1,2,3}
	for i:=0;i<len(a);i   {
		// 闭包函数内部使用传参参数的值。内部的值为传参的值。同时引用是不同的
		defer func(index int) {
		        // index 有一个新地址指向它
			fmt.Println(a[index]) // index == i
		}(i)
		// 后进先出,3 2 1
	}
}
// 控制台输出: 
//     3
//     2
//     1

坑9. defer所调用的非闭包函数,参数如果是函数,会按顺序先执行(函数参数)

代码语言:javascript复制
func calc(index string, a, b int) int {
	ret := a   b
	fmt.Println(index, a, b, ret)
	return ret
}
func defer6()  {
	a := 1
	b := 2
	// calc 充当了函数中的函数参数。即使在 defer 的函数中,它作为函数参数,定义的时候也会首先调用函数进行求值
	// 按照正常的顺序,calc("10", a, b) 首先被调用求值。calc("122", a, b) 排第二被调用
	defer calc("1", a, calc("10", a, b))
	defer calc("12",a, calc("122", a, b))
}
// 控制台输出:
/**
10 1 2 3   // 第一个函数参数
122 1 2 3  // 第二个函数参数
12 1 3 4   // 倒数第一个 calc
1 1 3 4    // 倒数第二个 calc
*/
注意
  • defer 不影响 return的值

参考1

参考2

0 人点赞