一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第9天,点击查看活动详情。
defer
defer 语句可以将一次函数调用压入栈中(实际底层可能是堆,也可能是栈,取决于场景,但一定是 LIFO)。通常被用来执行资源的清理和释放。官方spec
三条规则
- defer 函数的参数值是在defer语句处就被确定的
A deferred function’s arguments are evaluated when the defer statement is evaluated.
下面这段代码,虽然后续进行了i++,但其实已经没用了,defer fmt.Println(i) 的时候 i 的值就已经被确定下来为 0,所以最后打印的结果只能为 0 。
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
- defer 函数的调用顺序是【后进先出】
Deferred function calls are executed in Last In First Out order after the surrounding function returns.
下面的例子能很直观地反应这一点:
package main
import "fmt"
func main() {
fmt.Println("counting")
for i := 0; i < 10; i++ {
defer fmt.Println(i)
}
fmt.Println("done")
}
输出结果:
counting
done
9
8
7
6
5
4
3
2
1
0
- defer 函数可以对所在函数命名的返回值进行读写
Deferred functions may read and assign to the returning function’s named return values.
这里有一个很有意思的例子:
func c() (i int) {
defer func() { i++ }()
return 1
}
很多人以为结果是 1,毕竟不管在 defer 里怎么折腾,最后return 的是 1。
事实上,这里的结果是 2。
A "return" statement in a function
Fterminates the execution ofF, and optionally provides one or more result values. Any functions deferred byFare executed beforeFreturns to its caller.
根据官方spec,defer的函数一定是在所在函数返回前执行的。
需要注意的是,此处 return xxx 这样的语句,并不是一条原子指令。
return 语句在实际执行时需要经过三步:
- 给返回值赋值;
- 调用 defer 栈中的函数;
- 空 return 返回。
上面的 c 函数,在编译器看来,本质是这样的:
// 表面上
func c() (i int) {
defer func() { i++ }()
return 1
}
// 实际上
func c() (i int) {
i = 1
defer func() { i++ }()
return
}
这里再补充两个容易混淆的例子
// 表面上
func f() (r int) {
t := 5
defer func() {
t = t + 5
}()
return t
}
// 实际上
func f() (r int) {
t := 5
r = t //赋值指令
func() { //defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
t = t + 5
}
return //空的return指令,最后返回的还是5,不是10
}
// 表面上
func f() (r int) {
defer func(r int) {
r = r + 5
}(r)
return 1
}
// 实际上
func f() (r int) {
r = 1 //给返回值赋值
func(r int) { //这里改的r是传值传进去的r,不会改变要返回的那个r值
r = r + 5
}(r)
return //空的return,返回了 1,不是6
}
结论:defer确实是在return之前调用的。但表现形式上却可能不像。本质原因是return xxx语句并不是一条原子指令,defer被插入到了赋值 与 ret之间,因此可能有机会改变最终的返回值。
关于defer的底层实现原理,建议读一下这篇博客,写的很好:lailin.xyz/post/defer.…
panic
panic 是 Golang 自带的函数,可以终止正常的程序执行顺序,开始 panic。表现为原有函数执行停止,执行defer栈中的函数,然后返回。
从 caller 的视角来看,如果被调用的函数 F 出现了 panic(不管有意调用或是无意触发了 runtime error),表现上看都是 F 调用了 panic,然后层层往上抛。直到当前 goroutine 中,所有函数都返回。此时整个程序就会 crash。
recover
The recover built-in function allows a program to manage behavior of a panicking goroutine. Executing a call to recover inside a deferred function (but not any function called by it) stops the panicking sequence by restoring normal execution and retrieves the error value passed to the call of panic. If recover is called outside the deferred function it will not stop a panicking sequence. In this case, or when the goroutine is not panicking, or if the argument supplied to panic was nil, recover returns nil. Thus the return value from recover reports whether the goroutine is panicking.
与 panic 成对出现的 recover 也是 Golang 自带的函数,它的作用是重新获取一个已经发生panic的goroutine的控制。
-
recover 只能在 defer 函数中调用才有意义。如果在函数正常流程中调用,recover 会返回 nil,无其他作用。
-
recover 的返回值是调用 panic 时传入的 error。
我们直接来看官方 encoding/json 包中的用法:
317 // jsonError is an error wrapper type for internal use only.
318 // Panics with errors are wrapped in jsonError so that the top-level recover
319 // can distinguish intentional panics from this package.
320 type jsonError struct{ error }
321
322 func (e *encodeState) marshal(v any, opts encOpts) (err error) {
323 defer func() {
324 if r := recover(); r != nil {
325 if je, ok := r.(jsonError); ok {
326 err = je.error
327 } else {
328 panic(r)
329 }
330 }
331 }()
332 e.reflectValue(reflect.ValueOf(v), opts)
333 return nil
334 }
335
336 // error aborts the encoding by panicking with err wrapped in jsonError.
337 func (e *encodeState) error(err error) {
338 panic(jsonError{err})
339 }
如果出错,用 jsonError 作为 wrapper 来包装 error,这样调用方可以更好的感知错误的来源。