💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
什么是defer?如何理解 defer 关键字?Go 中使用 defer 的一些坑。 defer 意为延迟,在 golang 中用于延迟执行一个函数。它可以帮助我们处理容易忽略的问题,如资源释放、连接关闭等。但在实际使用过程中,有一些需要注意的地方. 1. 若函数中有多个 defer,其执行顺序为 先进后出,可以理解为栈。 ~~~go package main import "fmt" func main() { for i := 0; i < 5; i++ { defer fmt.Println(i) } } ~~~ 运行: ~~~go 4 3 2 1 0 ~~~ 2. return 会做什么呢? Go 的函数返回值是通过堆栈返回的, return 语句不是原子操作,而是被拆成了两步. * 给返回值赋值 (rval) * 调用 defer 表达式 * 返回给调用函数(ret) ~~~go package main import "fmt" func main() { fmt.Println(increase(1)) } func increase(d int) (ret int) { defer func() { ret++ }() return d } ~~~ 运行输出: ~~~go 2 ~~~ 3. 若 defer 表达式有返回值,将会被丢弃。 闭包与匿名函数. * 匿名函数:没有函数名的函数。 * 闭包:可以使用另外一个函数作用域中的变量的函数。 在实际开发中,defer 的使用经常伴随着闭包与匿名函数的使用。 ~~~go package main import "fmt" func main() { for i := 0; i < 5; i++ { defer func() { fmt.Println(i) }() } } ~~~ 运行输出: ~~~go 5 5 5 5 5 ~~~ 之所以这样是因为,defer 表达式中的 i 是对 for 循环中 i 的引用。到最后,i 加到 5,故最后全部打印 5。 如果将 i 作为参数传入 defer 表达式中,在传入最初就会进行求值保存,只是没有执行延迟函数而已。 应用示例: ~~~go func f1() (result int) { defer func() { result++ }() return 0 } ~~~ ~~~go func f2() (r int) { t := 5 defer func() { t = t + 5 }() return t } ~~~ ~~~go func f3() (r int) { defer func(r int) { r = r + 5 }(r) return 1 } ~~~ ~~~go type Test struct { Max int } func (t *Test) Println() { fmt.Println(t.Max) } func deferExec(f func()) { f() } func call() { var t *Test defer deferExec(t.Println) t = new(Test) } ~~~ 有没有得出结果?例1的答案不是 0,例2的答案不是 10,例3的答案也不是 6。 defer是在return之前执行的。这个在[官方文档](https://golang.org/ref/spec#defer_statements)中是明确说明了的。要使用defer时不踩坑,最重要的一点就是要明白,`return xxx`这一条语句并不是一条原子指令! ~~~gfm 函数返回的过程是这样的:先给返回值赋值,然后调用defer表达式,最后才是返回到调用函数中。 defer表达式可能会在设置函数返回值之后,在返回到调用函数之前,修改返回值,使最终的函数返回值与你想象的不一致。 其实使用defer时,用一个简单的转换规则改写一下,就不会迷糊了。改写规则是将return语句拆成两句写,return xxx会被改写成: 返回值 = xxx 调用defer函数 空的return ~~~ f1: 比较简单,参考结论2,将 0 赋给 result,defer 延迟函数修改 result,最后返回给调用函数。正确答案是 1。 f1可以修改成长这样的: ~~~go func f() (result int) { result = 0 // return语句不是一条原子调用,return xxx其实是赋值+ret指令 func() { // defer被插入到return之前执行,也就是赋返回值和ret指令之间 result++ }() return } ~~~ 所以这个返回值是1。 f2: defer 是在 t 赋值给 r 之后执行的,而 defer 延迟函数只改变了 t 的值,r 不变。正确答案 5。 f2可以修改成这样的: ~~~go func f() (r int) { t := 5 r = t // 赋值指令 func() { // defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过 t = t + 5 } return // 空的return指令 } ~~~ 所以这个的结果是5。 f3: 这里将 r 作为参数传入了 defer 表达式。故 func (r int) 中的 r 非 func f() (r int) 中的 r,只是参数命名相同而已。正确答案 1。 f3可以修改成这样的: ~~~go func f() (r int) { r = 1 // 给返回值赋值 func(r int) { // 这里改的r是传值传进去的r,不会改变要返回的那个r值 r = r + 5 }(r) return // 空的return } ~~~ 所以这个例子的结果是1。 f4: 这里将发生 panic。将方法传给 deferExec,实际上在传的过程中对方法求了值。而此时的 t 任然为 nil。 因此, defer确实是在return之前调用的。但表现形式上却可能不像。根本原因是`return xxx`语句并不是一条原子指令,defer被插入到了赋值 与 ret之间,因此可能有机会改变最终的返回值。 defer关键字的实现跟go关键字很类似,不同的是它调用的是`runtime.deferproc`而不是`runtime.newproc`。 在defer出现的地方,插入了指令`call runtime.deferproc`,然后在函数返回之前的地方,插入指令`call runtime.deferreturn`。 普通的函数返回时,汇编代码类似: ~~~go add xx SP return ~~~ 如果其中包含了defer语句,则汇编代码是: ~~~go call runtime.deferreturn, add xx SP return ~~~ goroutine的控制结构中,有一张表记录defer,调用`runtime.deferproc`时会将需要defer的表达式记录在表中,而在调用`runtime.deferreturn`的时候,则会依次从defer表中出栈并执行。