0%

Go defer详解

Go defer详解

什么是defer

在进行编程的时候,经常需要在代码中申请一些资源,比如数据库连接,文件句柄等等,这些资源需要在用完之后进行释放,否则会造成内存泄漏(内存一直被占用而不被释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果),而Go就提供了一个defer关键字,可以实现延迟调用。defer可以让函数或者语句在当前函数执行完毕后执行(包括程序正常退出和panic导致的异常结束)

以下代码有什么问题?

r.mu.Lock()

rand.Intn(param)

r.mu.Unlock()

看起来这段代码是没有什么问题的,但这是建立在程序正常运行的基础上,如果rand.Intn出现了异常而造成了panic,就会出现r一直占用着锁而不释放的问题,所以即使是简单的释放资源的代码,使用defer也是很有必要的。

defer的执行顺序

defer的语句并不会马上执行,而是会进入一个栈,所以执行的顺序也会和栈一样,即先进后出,也就是说最先被定义的defer语句最后执行,这个原因是后面定义的函数可能对前面的资源有依赖,所以要先执行;否则如果前面先执行了,对某些资源进行释放,后面的函数就会出错。

在defer函数定义时,对外部变量的引用有两种方式,函数参数和闭包引用,前者在defer定义时就把值(广义的值,如果传的是引用类型,那么和定义的时候可能不一样)传给defer,并被存起来,而后者则会在defer真正调用的时候根据上下文确定值。

如果是闭包的话,则会和定义的时候不一样,看下面这个栗子

1
2
3
4
5
6
7
8
9
10
11
func main() {
var t [3]struct{}
for i := range t {
defer func() {
fmt.Println(i) //defer 后面接的是闭包 在for 循环结束后的i的值为2
}()
}
// 2
// 2
// 2
}

注意:在Go使用close()关闭某些资源的时候,最好提前判断调用主体是否为空,否则很可能会解引用一个空指针,从而造成panic的问题

return与defer

return并不是一条原子指令,return的执行顺序是这样的

1.返回值=xxx

2.调用defer函数

3.ret指令

下面是两道defer经典题

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
29
30
31
32
func f1() int {  //返回值没有取名
x := 5
defer func() {
x++
}()
return x //1.返回值赋值 2.defer 3.执行RET返回 //开辟了一份空间 return语句先将5赋值给x defer修改了x的值 但是RET命令执行的对象还是那个‘5’
}
func f2() (x int) { //返回值有名字
defer func() {
x++ //闭包
}()
return 5 //修改的是x的值
}
func f3() (y int) {
x := 5
defer func() {
x++
}()
return x //y = x = 5 defer修改的是 x 真正返回的y还是5
}
func f4() (x int) {
defer func(x int) {
x++ //改的是函数值的副本 //如果这里是 defer func(x *int) { x ++ }(x)
}(x) //那么结果就会变成6
return 5
}
func main() {
fmt.Println(f1()) //5
fmt.Println(f2()) //6
fmt.Println(f3()) //5
fmt.Println(f4()) //5
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
x := 1
y := 2
defer calc("AA", x, calc("A", x, y))
x = 10
defer calc("BB", x, calc("B", x, y))
y = 20
//1.调用calc中的calc("A",x,y) 输出:"A",1,2,3 因为函数调用时会确定每个参数的值
//2.defer calc("AA",x,3)
//3.调用第二个calc中的calc("B",x,y) 此时x y是20 输出:"B",10,2,12
//4.defer calc("BB",x,12)
//5.先进后出 调用第二个calc 输出:"BB",10,12,22
//6.调用第二个calc 输出:"AA" 1,3,4
}

什么是闭包

闭包=函数+引用环境 不推荐在生产环境中使用,就算你自己对闭包的使用很熟练,但是代码写出来是给人看的,不能苛求你的同事、测试对这部分也很熟悉,而且闭包很容易出现错误。

Go中的所有匿名函数都是闭包程序

defer配合recover使用

执行初始化的时候出问题,最好直接panic掉,避免上线之后出现更大的问题,有些时候,需要从异常中恢复,比如服务器的严重问题产生了panic,这个时候需要在程序崩溃之前做一些扫尾工作,比如关闭客户端的连接。并且一个panic不应该影响整个服务器的运行,这时候就需要defer 和 recovery进行配合

defer配合recovery使用:recovery必须在defer的函数中 才是标准格式

比如这种

func main() {
defer f()
panic(404)
}

func f() {
if err := recover(); err != nil {
fmt.Println("recover")
return
}
}

或者采用匿名函数

func main() { defer func() { if err := recover(); err != nil { fmt.Println("recover") return } }() panic(404) }

但是注意,一定要在函数里,像这样是不行的

func main() { defer recover() panic(404) }//还是会panic

defer链是如何被执行的

前面我们说到过,一个函数中的defer语句是按照栈的顺序执行的,每一条defer语句都会创建一个_defer结构体,这些结构体以链表的形式挂载在G下(Goroutine)。

defer首先会调用deferporc函数,new一个_defer结构体,挂到G上,当然,调用new之前会从当前G绑定的P中的defer pool中取,如果没有的话则会去全局的defer pool中取,是在没有就新建一个,这是Go runtime的常规操作,也就是设置多级缓存,提高运行效率。

之后按照顺序,处理一个个_defer结构体,即完成了defer链的执行。