defer是什么
defer是Go语言的一个关键字,defer关键字后跟的函数或者方法能够延迟到执行return或panic时再进行调用。
defer的使用方法
defer func(aoth)
这样就能注册一个defer函数,使其在函数结束之前进行调用,参数和函数名都会固定。
defer的底层结构
当我们在使用defer函数的时候,编译器会生成一个_defer的结构体,一个函数中可能会存在多次的defer调用,多个_defer结构体之间是以链式存储的方式存储的,最终形成一个_defer的链表。而一个goroutine中的_defer字段就是指向_defer结构体的头节点。
这个过程中,每当创建一个defer结构体,就会从表头进行插入。当执行return函数时,又会从表头依次执行,FILO。
defer的执行过程
defer在执行过程中,会在底层插入两种函数:
- defer内存分配函数:deferproc(堆内存分配)和deferprocStack(栈分配)
- defer执行函数:deferreturn
defer相关的处理逻辑相关源码在cmd/compile/internal/ssagen/ssa.go文件中的state.stmt()方法中。
defer的内存分配
defer内存分配的过程主要有三种方式:
- 内联分配
- 栈上分配
- 堆上分配
defer在分配内存的过程中分配的位置不一样,意为这分配时使用的函数也不相同。在栈上分配时使用deferprocStack函数,而堆上分配时使用deferproc函数。
在堆上分配
相应源码在src/runtime/panic.go文件中。
defer在堆上分配时,运用到了内存池的思想。首先会在逻辑处理器p本地和全局的defer缓存池中找到可用的_defer结构体,找不到之后才会在堆上新创建一个。(源码中newdefer()函数部分)比较常见的情况是在defer调用的函数有创建新对象或切片的操作。
栈上分配
栈上分配主要用的是deferprocStack()函数,go语言在编译过程中如果判断_defer需要在栈上分配,编译器会直接在函数调用栈上初始化_defer记录, 并作为参数传递给deferprocStack()函数。常见的情况是defer调用的函数涉及一些简单的操作如函数调用,修改局部变量。
内联分配
内联分配主要是go语言在编译阶段,会将一些确定的defer函数直接与代码块进行关联,直接在函数末尾对defer进行调用,减少函数调用的开销。
内联分配会进行defer是否在循环体内调用的判断,因为defer在循环体内调用,defer不确定循环体会调用几次,会存在逃逸到堆上的情况,这样会导致性能下降的问题。
从源码中总结如下:
一般情况下,go会优先使用内联的方式进行defer结构体的内存分配,但是需要满足一下几个条件:
- build编译的时候没有设置-N
- defer函数个数没有超过8个
- defer所在函数返回值个数和defer函数个数乘积不超过15个
- defer没有出现在循环语句中
内联分配相应源码在cmd/compile/internal/walk/stmt.go文件中的walkStmt()函数和cmd/compile/internal/ssagen/ssa.go的buildssa()函数中。
总的来说,会优先选择内联分配的方式进行内存分配。当内联分配不满足,且没有发生内存逃逸时会选择栈上分配的方式。当发生内存逃逸,则使用堆上分配的方式。
defer能修改返回值么,defer与return的关系是怎么样的?
defer能返回修改值,当defer调用的函数有显示返回值的时候,是可以修改返回值的。
return的过程一般是这样:计算返回值(如果有的话),执行defer语句,进行实际的返回,所以defer语句会在return前执行。如果defer调用的函数存在显示返回值时,会对return的返回值造成影响。