基础
defer 是 Go 语言提供的一种用于注册延迟调用的机制:让函数或语句可以在当前函数执行完毕后(包括通过 return 正常结束或者 panic 导致的异常结束)执行。
- 每次 defer 语句执行的时候,会把函数“压栈”,函数参数会被拷贝下来。
- 当外层函数退出时,defer 函数按照定义的逆序执行(先进后出)。
- 如果 defer 执行的函数为 nil, 那么会在最终调用函数的产生 panic。
不能在defer里面返回值
defer 函数对外部变量的引用
在 defer 函数定义时,对外部变量的引用是有两种方式的,分别是作为函数参数和作为闭包引用。
- 作为函数参数,则在 defer 定义时就把值传递给 defer,并被 cache 起来。
- 作为闭包引用的话,则会在defer 函数真正调用时根据整个上下文确定当前的值。
defer 后面的语句在执行的时候,函数调用的参数会被保存起来,也就是复制了一份。真正执行的时候,实际上用到的是这个复制的变量,因此如果此变量是一个“值”,那么就和定义的时候是一致的。如果此变量是一个“引用”,那么就可能和定义的时候不一致。
defer 后面跟的是闭包,必然是引用类型的变量。
执行的时机
什么情况下会调用 defer 延迟过的函数呢?
- 当函数执行了
return
语句后
- 当函数处于
panicing
状态,也就是程序中 panic
回溯上来到当前函数范围内时
对于第一条,首先要了解到 return xxx
不是原子命令,会被拆分成如下三行。
返回值 = xxx
调用 defer 函数
空的 return
defer 会被插入到赋值和返回之间执行。 来看看如何拆解。
func f() (r int) {
t := 5
defer func() {
t = t + 5
}()
return t
}
// 拆解后
func f() (r int) {
t := 5
r = t // 1. 赋值指令
func() {
t = t + 5 // 2. defer 被插入到赋值与返回之间执行,这个例子中返回值 r 没被修改过
}
return // 3. 空的 return 指令
}
func f() (r int) {
defer func(r int) {
r = r + 5
}(r)
return 1
}
// 拆解后
func f() (r int) {
r = 1 // 1. 赋值
func(r int) {
r = r + 5 // 2. 这里改的 r 是之前传值传进去的r,不会改变要返回的那个r值
}(r)
return // 3. 空的 return
}
defer 和 闭包
其实面试时候,考察是否掌握了 defer 无非是看 defer 的函数是否是闭包。以及 return 语句的拆解。
func f1() {
var err error
defer fmt.Println(err)
err = errors.New("defer error")
return
}
func f2() {
var err error
defer func() {
fmt.Println(err)
}()
err = errors.New("defer error")
return
}
func f3() {
var err error
defer func(err error) {
fmt.Println(err)
}(err)
err = errors.New("defer error")
return
}
func main() {
f1()
f2()
f3()
}
这段函数输出值如下:
f3() 很多人会搞错,以为也会输出 defer error
。但只要学会区分什么是闭包,就不会发生拿不准返回值的问题。
使用
资源的释放
一些成对操作,需要回收资源的场景:打开连接/关闭连接;加锁/释放锁;打开文件/关闭文件等。
resA, err:= getA()
if err != nil {
return
}
// 这里是经典范式:先判断对错,再释放资源。
defer ReleaseA()
resB,err := getB(resA)
if err != nile {
return
}
defer ReleaseB()
异常处理
和 panic
以及 recover
组合起来模拟 try...catch
功能。
func protect(g func()) {
defer func() {
log.Println("done") // Println executes normally even if there is a panic
if x := recover(); x != nil {
log.Printf("run time panic: %v", x)
}
}()
log.Println("start")
g()
}
当 g()
中通过 panic 抛出错误时,会在 defer 中用 recover 进行捕获。也就是在子函数中的 panic 触发了其处于 panicing 状态,从而当 panic 回溯到当前函数时调用本函数的 defer 修饰的函数。
Go 官方不建议这么做。
考题
defer 的面试题一般都会考察 匿名返回值 和 命名返回值 之间的区别。
匿名返回值
func main() {
fmt.Println("a return:", a()) // 打印结果为 a return: 0
}
func a() int {
var i int
defer func() {
i++
fmt.Println("a defer2:", i) // 打印结果为 a defer2: 2
}()
defer func() {
i++
fmt.Println("a defer1:", i) // 打印结果为 a defer1: 1
}()
return i
}
输出结果:
a defer1: 1
a defer2: 2
a return: 0
匿名返回值是在 return 之前被声明(鉴于类型原因,类型零值为0),defer 无法访问匿名的返回值,因此返回值是 0,而 defer 还是操作之前定义好的变量 i。
命名返回值
func main() {
fmt.Println("a return:", a()) // 打印结果为 b return: 2
}
func a() (i int) {
defer func() {
i++
fmt.Println("a defer2:", i) // 打印结果为 b defer2: 2
}()
defer func() {
i++
fmt.Println("a defer1:", i) // 打印结果为 b defer1: 1
}()
return i // 或者直接 return 效果相同
}
输出结果:
a defer1: 1
a defer2: 2
a return: 2
命名返回值是在函数声明的同时被声明。因此 defer 可以访问命名返回值。return 返回后的值其实是 defer 修改后的值。
作用域
在什么环境下就不会执行?
- 当任意一条(主)协程发生 panic 时,会执行当前协程中 panic 之前已声明的 defer;
- 主动调用
os.Exit(int)
退出进程时,defer 将不再被执行
- 在发生 panic 的(主)协程中,如果没有一个 defer 调用 recover()进行恢复,则会在执行完之前已声明的 defer 后,引发整个进程崩溃;
- defer 只对当前(主)协程有效
相关链接