golang-defer

defer的使用特点

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

关于官方解释

1
2
3
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”。
1
2
3
4
5
6
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
  1. 在周围函数返回后,延迟函数调用以后进先出顺序执行。
1
2
3
4
5
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
} //将会打印3210

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
}

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

  要搞清这个问题首先需要了解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,如果资源没有获取成功,即没有必要也不应该再对资源执行释放操作。如果不判断获取资源是否成功就执行释放操作的话,还有可能导致释放方法执行错误。

正确做法

1
2
3
4
5
6
7
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并不会被执行。

1
2
3
4
5
6
func deferExit() {
defer func() {
fmt.Println("defer")
}()
os.Exit(0)
}

上面的defer并不会输出。

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
func p(a int)  {
fmt.Println(a)
}

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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所调用的非闭包函数,参数如果是函数,会按顺序先执行(函数参数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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的值

参考

参考

分享到 评论