Go defer的坑

常见问题

defer常见用法

1.使用defer Run(Run(1))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func Run(i int)int{
fmt.Println("run")
return 1
}
// 非匿名函数
func main() {
defer Run(Run(1))
fmt.Println("exec")
}
/**
output:
run
exec
run
*/

2.使用defer fun(){...}()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
// 匿名函数
defer fun(){
Run(Run(1))
}()
fmt.Println("exec")
}
/**
output:
exec
run
run
*/

结论:
上述两种方式都是defer的正确用法,方式1常用于在做函数开始时调整环境,函数结束时恢复环境的操作,方式2常用于在函数结束时批量清理操作

延伸:go源码中利用defer实现调整环境并后续恢复现场的操作

1
2
3
4
5
func TestPoolNew(t *testing.T) {
// disable GC so we can control when it happens.
defer debug.SetGCPercent(debug.SetGCPercent(-1))
...
}

debug.SetGCPercent被执行了两次,而且这个函数返回的是上一次GC的值,因此defer在这里的用途是还原到调用此函数之前的GC设置,也就是恢复现场

defer返回值的坑

  • 多个defer的执行顺序为“后进先出”;
  • defer、return、返回值三者的执行逻辑应该是:return最先执行,return负责将结果写入返回值中;接着defer开始执行一些收尾工作;最后函数携带当前返回值退出。

return并非原子操作,分为赋值和返回值两步操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//不带命名返回值的函数
//return默认指定了一个返回值(假设为s),首先将i赋值给s,然后defer对i进行操作,最后返回s,所以是0
package main
import "fmt"
func main() {
fmt.Println("return:", test())// defer 和 return之间的顺序是先返回值, i=0,后defer
}
func test() int {//这里返回值没有命名
var i int
defer func() {
i++
fmt.Println("defer1", i) //作为闭包引用的话,则会在defer函数执行时根据整个上下文确定当前的值。i=2
}()
defer func() {
i++
fmt.Println("defer2", i) //作为闭包引用的话,则会在defer函数执行时根据整个上下文确定当前的值。i=1
}()
return i
}
/**
defer2 1
defer1 2
return: 0
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//带命名返回值的函数
//return i,defer对i进行操作,最后返回i,所以是2
package main
import "fmt"
func main() {
fmt.Println("return:", test())
}
func test() (i int) { //返回值命名i
defer func() {
i++
fmt.Println("defer1", i)
}()
defer func() {
i++
fmt.Println("defer2", i)
}()
return i
}
/**
defer2 1
defer1 2
return: 2
*/

Go —— defer 和 return 执行的先后顺序