关于官方解释
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调用的参数会立即计算,但在周围函数返回之前不会执行函数调用。
以及延迟函数调用被压入堆栈。当函数返回时,其延迟调用以后进先出顺序执行。
像这样一段代码
func A(){
defer B()
// code to do something
}
编译后的伪指令是这样的
func f1() {
r := runtime.deferproc(0, A) // 经过recover返回时r为1,否则为0
if r > 0 {
goto ret
}
// code to do something
runtime.deferreturn()
return
ret:
runtime.deferreturn()
}
其中与defer指令相关的有两个部分。第一部分是deferproc,它负责保存要执行的函数信息,我们称之为defer“注册”。
func deferproc(siz int32, fn *funcval)
从函数原型来看,deferproc函数有两个参数,第一个是被注册的defer函数的参数加返回值共占多少字节;第二个参数是一个runtime.funcval结构体的指针,也就是一个Function Value。
与defer指令相关的第二部分就是deferreturn,它被编译器插入到函数返回以前调用,负责执行已经注册的defer函数。所以defer函数之所以能延迟到函数返回前执行,就是因为先注册,后调用。
defer信息会注册到一个链表,而当前执行的goroutine持有这个链表的头指针,每个goroutine在运行时都有一个对应的结构体g、其中有一个字段,指向defer链表头,defer链表链接起来的是一个一个 _defer结构体,新注册的defer会添加到链表头,执行时也是从头开始,所以defer才会表现为倒序执行。
defer结构
type _defer struct {
siz int32
started bool
sp uintptr // sp at time of defer
pc uintptr
fn *funcval
_panic *_panic // panic that is running defer
link *_defer
}
siz:由deferproc第一个参数传入,就是defer函数参数加返回值的总大小。这段空间会直接分配在_defer结构体后面,用于在注册时保存给defer函数传入的参数,并在执行时直接拷贝到defer函数的调用者栈上。
started :标识defer函数是否已经开始执行;
sp:就是注册defer函数的函数栈指针;
pc:是deferproc函数返回后要继续执行的指令地址;
fn:由deferproc的第二个参数传入,也就是被注册的defer函数;
_panic:是触发defer函数执行的panic指针,正常流程执行defer时它就是nil;
link:自然是链到之前注册的那个_defer结构体。
defer函数的参数是如何传递的。
func A1(a int) {
fmt.Println(a)
}
func A() {
a, b := 1, 2
defer A1(a)
a = a + b
fmt.Println(a, b)
}
这个例子中,函数A注册了一个defer函数A1,在A的函数栈帧中,局部变量区域存储a=1,b=2。
到deferproc函数注册defer函数A1时:
func deferproc(siz int32, fn *funcval)
第一个参数是A1的参数加返回值共占多少字节。A1没有返回值,64位下一个整型参数占用8字节。
第二个参数是函数A1。没有捕获列表的Function Value,在编译阶段会做出优化,就是在只读数据段分配一个共用的funcval结构体。
如下图中,函数A1的指令入口地址为addr1。在只读数据段分配的指向A1指令入口的funcval结构体地址为addr2,所以deferproc函数第二个参数就是addr2。
额外要注意的是,deferproc函数调用时,编译器会在它自己的两个参数后面,开辟一段空间,用于存放defer函数A1的返回值和参数。这一段空间会在注册defer时,直接拷贝到_defer结构体的后面。
A1只有一个参数a=1,放在deferproc函数自己的两个参数之后。注意deferproc函数的返回值空间并没有分配在调用者栈上,而是放到了寄存器中,这和recover有关
deferproc函数执行,需要堆分配一段空间,用于放_defer结构体以及后面siz大小的参数与返回值。
_defer结构体的第一个字段,A1的参数加返回值共占8字节;defer函数尚未执行,所以started=false;sp就是调用者A的栈指针;pc就是deferproc函数的返回地址return addr;被注册的function value为A1;defer结构体后面的8字节用来保存传递给A1的参数。
defer函数的参数怎么传给deferproc
deferproc函数执行,需要堆分配一段空间,用于放_defer结构体以及后面siz大小的参数与返回值。
_defer结构体的第一个字段,A1的参数加返回值共占8字节;defer函数尚未执行,所以started=false;sp就是调用者A的栈指针;pc就是deferproc函数的返回地址return addr;被注册的function value为A1;defer结构体后面的8字节用来保存传递给A1的参数。
要注册的defer链表项
然后这个defer结构体就被添加到defer链表头,deferproc注册结束。
“频繁的堆分配势必影响性能,所以Go语言会预分配不同规格的deferpool,执行时从空闲defer中取一个出来用。没有空闲的或者没有大小合适的,再进行堆分配。用完以后,再放回空闲_defer池。这样可以避免频繁的堆分配与回收。”
**
按照如下函数A编译后的伪指令所示,deferproc结束后,接下来会执行到a=a+b这一步,所以,局部变量a被置为3。接下来会输出:a=3,b=2。
//函数A编译后的伪指令
func A() {
a, b := 1, 2
r := runtime.deferproc(8, A1,1)
if r > 0 {
goto ret
}
a = a + b
fmt.Println(a, b)//3,2
runtime.deferreturn()//执行defer链表
return
ret:
runtime.deferreturn()
}
然后就到deferreturn执行defer链表这里了。从当前goroutine找到链表头上的这个_defer结构体,通过_defer.fn找到defer函数的funcval结构体,进而拿到函数A1的入口地址。接下来就可以调用A1了。
调用A1时,会把_defer后面的参数与返回值整个拷贝到A1的调用者栈上。然后A1开始执行,输出参数值a=1。
这个例子就到这里,关键是理解defer函数的参数在注册时拷贝到堆上,执行时再拷贝到栈上。
既然deferproc注册的是一个Function Value,而这次的例子是没有捕获列表的Function Value
defer 特性:
1. 关键字 defer 用于注册延迟调用。
2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
3. 多个defer语句,按先进后出的方式执行。
4. defer语句中的变量,在defer声明时就决定了。
defer用途:
1. 关闭文件句柄
2. 锁资源释放
3. 数据库连接释放
defer指令对应着2部分
其中deferproc负责把执行的函数信息保存起来—defer注册
直到返回之前通过deferreturn执行注册的defer函数,先注册后调用,才实现了延迟调用的效果
defer信息会注册到一个链表,而当前执行的goroutine持有当前这个链表的头指针
它有如何特点
- 所在的函数中,它在 return 或 panic 或 执行完毕 后被调用
- 多个 defer,它们的被调用顺序,为栈的形式。先进后出,先定义的后被调用
看下面几个例子:
在计算defer语句时,将计算延迟函数的参数。在此示例中,在延迟Println调用时计算表达式“i”。函数返回后,延迟调用将打印“0”。
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
在周围函数返回后,延迟函数调用以后进先出顺序执行。
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
} //将会打印3210
然后不免在使用过程中会遇到这些坑
坑1. defer在匿名返回值和命名返回值函数中的不同表现
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
}
func main() {
var whatever [5]struct{}
for i := range whatever {
defer func() { fmt.Println(i) }()
}
}
下面的方法输出4,4,4,4,4
上面的方法会输出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,如果资源没有获取成功,即没有必要也不应该再对资源执行释放操作。如果不判断获取资源是否成功就执行释放操作的话,还有可能导致释放方法执行错误。
正确做法
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并不会被执行。
func deferExit() {
defer func() {
fmt.Println("defer")
}()
os.Exit(0)
}
上面的defer并不会输出。
坑4.非引用传参给defer调用的函数,且为非闭包函数,值不会受后面的改变影响
func defer0() {
a := 1 // a 作为演示的参数
defer fmt.Println(a) // 非引用传参,非闭包函数中,a 的值 不会 受后面的改变影响
a = a + 2
}
// 控制台输出 1
坑5. 传递引用给defer调用的函数,即使不使用闭包函数,值也会受后面的改变影响
func myPrintln(point *int) {
fmt.Println(*point) // 输出引用所指向的值
}
func defer1() {
a := 3
// &a 是 a 的引用。内存中的形式: 0x .... ---> 3
defer myPrintln(&a) // 传递引用给函数,即使不使用闭包函数,值 会 受后面的改变影响
a = a + 2
}
// 控制台输出 5
坑6. 传递值给defer调用的函数,且非闭包函数,值不会受后面的改变影响
func p(a int) {
fmt.Println(a)
}
func defer2() {
a := 3
defer p(a) // 传递值给函数,且非闭包函数,值 不会 受后面的改变影响
a = a + 2
}
// 控制台输出: 3
坑7. defer调用闭包函数,且内调用外部非传参进来的变量,值会受后面的改变影响
// 闭包函数内,事实是该值的引用
func defer3() {
a := 3
defer func() {
fmt.Println(a) // 闭包函数内调用外部非传参进来的变量,事实是该值的引用,值 会 受后面的改变影响
}()
a = a + 2 // 3 + 2 = 5
}
// 控制台输出: 5
坑8. defer调用闭包函数,若内部使用了传参参数的值。使用的是值
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所调用的非闭包函数,参数如果是函数,会按顺序先执行(函数参数)
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谁先谁后
package main
import "fmt"
func deferFunc() int {
fmt.Println("defer func called")
return 0
}
func returnFunc() int {
fmt.Println("return func called")
return 0
}
func returnAndDefer() int {
defer deferFunc()
return returnFunc()
}
func main() {
returnAndDefer()
}
return func called
defer func called
结论为:return之后的语句先执行,defer后的语句后执行
遇到panic时,遍历本协程的defer链表,并执行defer。在执行defer过程中:遇到recover则停止panic,返回recover处继续往下执行。如果没有遇到recover,遍历完本协程的defer链表后,向stderr抛出panic信息。
package main
import (
"fmt"
)
func main() {
defer_call()
fmt.Println("main 正常结束")
}
func defer_call() {
defer func() { fmt.Println("defer: panic 之前1") }()
defer func() { fmt.Println("defer: panic 之前2") }()
panic("异常内容") //触发defer出栈
defer func() { fmt.Println("defer: panic 之后,永远执行不到") }()
}
defer: panic 之前2
defer: panic 之前1
panic: 异常内容
//... 异常堆栈信息
defer 碰上闭包
package main
import "fmt"
func main() {
var whatever [5]struct{}
for i := range whatever {
defer func() { fmt.Println(i) }()
}
}
输出 4,4,4,4,4
多个 defer 注册,按 FILO 次序执行 ( 先进后出 )。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。
package main
func test(x int) {
defer println("a")
defer println("b")
defer func() {
println(100 / x) // div0 异常未被捕获,逐步往外传递,最终终止进程。
}()
defer println("c")
}
func main() {
test(0)
}
输出
c
b
a
panic: runtime error: integer divide by zero
延迟调用参数在注册时求值或复制,可用指针或闭包 “延迟” 读取。
package main
func test() {
x, y := 10, 20
defer func(i int) {
println("defer:", i, y) // y 闭包引用
}(x) // x 被复制
x += 10
y += 100
println("x =", x, "y =", y)
}
func main() {
test()
}
输出结果
x = 20 y = 120
defer: 10 120
滥用 defer 可能会导致性能问题,尤其是在一个 “大循环” 里。
package main
import (
"fmt"
"sync"
"time"
)
var lock sync.Mutex
func test() {
lock.Lock()
lock.Unlock()
}
func testdefer() {
lock.Lock()
defer lock.Unlock()
}
func main() {
func() {
t1 := time.Now()
for i := 0; i < 10000; i++ {
test()
}
elapsed := time.Since(t1)
fmt.Println("test elapsed: ", elapsed)
}()
func() {
t1 := time.Now()
for i := 0; i < 10000; i++ {
testdefer()
}
elapsed := time.Since(t1)
fmt.Println("testdefer elapsed: ", elapsed)
}()
}
输出
test elapsed: 223.162µs
testdefer elapsed: 781.304µs
defer 与 closure
package main
import (
"errors"
"fmt"
)
func foo(a, b int) (i int, err error) {
defer fmt.Printf("first defer err %v\n", err)
defer func(err error) { fmt.Printf("second defer err %v\n", err) }(err)
defer func() { fmt.Printf("third defer err %v\n", err) }()
if b == 0 {
err = errors.New("divided by zero!")
return
}
i = a / b
return
}
func main() {
foo(2, 0)
}
输出
third defer err divided by zero!
second defer err <nil>
first defer err <nil>
defer 与 return
package main
import "fmt"
func foo() (i int) {
i = 0
defer func() {
fmt.Println(i)
}()
return 2
}
func main() {
foo()
}
输出 2
在有具名返回值的函数中(这里具名返回值为 i),执行 return 2 的时候实际上已经将 i 的值重新赋值为 2。所以defer closure 输出结果为 2 而不是 1。
注意
- defer 不影响 return的值
- defer 最大的功能是 Panic 后依然有效
练习题
package main
import "fmt"
func DeferFunc1(i int) (t int) {
t = i
defer func() {
t += 3
}()
return t
}
// 将返回值t赋值为传入的i,此时t为1
// 执行return语句将t赋值给t(等于啥也没做)
// 执行defer方法,将t + 3 = 4
// 函数返回 4
// 因为t的作用域为整个函数所以修改有效。
func DeferFunc2(i int) int {
t := i
defer func() {
t += 3
}()
return t
}
// 创建变量t并赋值为1
// 执行return语句,注意这里是将t赋值给返回值,此时返回值为1(这个返回值并不是t)
// 执行defer方法,将t + 3 = 4
// 函数返回返回值1
func DeferFunc3(i int) (t int) {
defer func() {
t += i
}()
return 2
}
func DeferFunc4() (t int) {
defer func(i int) {
fmt.Println(i)
fmt.Println(t)
}(t)
t = 1
return 2
}
// 初始化返回值t为零值 0
// 首先执行defer的第一步,赋值defer中的func入参t为0
// 执行defer的第二步,将defer压栈
// 将t赋值为1
// 执行return语句,将返回值t赋值为2
// 执行defer的第三步,出栈并执行
// 因为在入栈时defer执行的func的入参已经赋值了,此时它作为的是一个形式参数,所以打印为0;相对应的因为最后已经将t的值修改为2,所以再打印一个2
func main() {
fmt.Println(DeferFunc1(1))
fmt.Println(DeferFunc2(1))
fmt.Println(DeferFunc3(1))
DeferFunc4()
}
参考