关于defer,找到了两篇官方的文章
defer
是Go语言的一种延迟调用的机制,可以在函数执行完毕之后执行动作。
在函数return
或者painc
(这两种情况下,当前函数执行结束了,这也算函数执行完毕之后)执行。
先看一个例子
// 文件copy
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
dst, err := os.Create(dstName)
if err != nil {
return
}
written, err = io.Copy(dst, src)
// 关闭文件
dst.Close()
src.Close()
return
}
上面的代码可以实现正确的功能,但有问题,如果创建dst
创建失败,src
文件就不会被关闭,对于上面的代码,我们只需要在return的时候增加close的代码就可以,但对于复杂的情况很不好处理。
在程序中必然存在一起资源清理、锁释放等操作,这些操作要在正常的操作结束之后,才可以执行,defer就解决这个问题
,Java中用try resource
可以处理。
对于上面的例子,改动如下:
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
// 先判断错误,在defer关闭
if err != nil {
return
}
defer src.Close()
dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close()
return io.Copy(dst, src)
}
需要注意,先判断错误,在defer关闭。
defer也有一定的执行规则,具体解释如下:
引用官方对defer的解释如下
Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked. Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred. That is, if the surrounding function returns through an explicit return statement, deferred functions are executed after any result parameters are set by that return statement but before the function returns to its caller. If a deferred function value evaluates to
nil
, execution panics when the function is invoked, not when the “defer” statement is executed.
这里要说明,defer语句执行和defer语句调用
defer语句执行:上面代码中defer dst.close
这就是defer语言执行,在这个时候还没有调用dst.close
方法,只是执行了一下defer语句。
defer语句调用:函数返回之前,真正执行defer中的操作。
翻译如下:
每一次defer语句执行的时候,函数的参数都会被计算一下并且保存在起来,但并没有函数的调用,也就是没defer语句调用,在defer所在的外层函数返回(return或者panic),defer会按照定义的顺序倒序调用。
defer执行的时机是外层函数已经设置好了返回值,返回到调用者的时候i,defer就开始执行。
如果defer 后面的函数是一个nil,在defer语句执行的时候会panic
从上面的官方定义知道,defer执行的时机如下:
设置返回值
defer调用
函数返回
defer执行的伪代码 如下:
defer定义的例子:
func main() {
opera1 := deferOpera1()
println(opera1) //output: 12
}
func deferOpera1() int {
var res = 12
defer func(res int) {
res += 1
}(res)
return res
}
伪代码:
func main() {
opera1 := deferOpera1()
println(opera1)
}
func deferOpera1() int {
var aRes int // 最终返回值
var res = 12
aRes = res // 对应的是 return res
func(res int) {
res += 1
}(res) // 在执行defer的时候已经保存了当时计算并且保存了,这个res输入此函数的参数,和外面的返回值没有关系
return // 函数返回给调用者
}
再来一个
func f() (r int) {
t := 5
defer func() {
t = t + 5
}()
return t
}
伪代码:
func f() (r int) {
t := 5
// 1. 赋值指令
r = t
// 2. defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
func() {
t = t + 5
}
// 3. 空的return指令
return
}
代码
func a() {
i := 0
defer fmt.Println(i) // 0
i++
return
}
defer执行的时候函数的参数已经计算了,保存的是当时变量的值,就是0。
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}
输出为3210
func c() (i int) {
defer func() { i++ }()
return 1
}
// output:2
c函数的返回值是由返回值变量名的,可以直接读取
defer的使用方式常见的有下面两种
defer 函数不需要操纵外部函数变量
defer deferOperation1()
defer func(key string){
对key做处理
}(实参)
defer函数里面用到了外部变量,并且通过defer的形参传递了过来。
defer 操作外部函数变量
defer func() {
// 这个里面可能引用了外部变量,但没有通过自己的形参传递进来
}()
defer函数里面用到了外部变量。没有通过形参传递过来,直接在defer里面调用了外部变量。
这叫做闭包
闭包
是代码+上下文
上下文:闭包函数里面引用的外部变量,这些变量的值是共用的,”引用传递“
代码:
func main() {
f := closer1() // 返回一个闭包
println(f()) // 每次调用都会在之前的基础上+1
println(f())
println(f())
println(f())
}
// output: 1,2,3,4
// 每次+1
func closer1() func() int {
var a = 0
return func() int{
a += 1
return a
}
}
对于上面的代码,a就是在闭包中的上下文,并且每次a都是之前的值,a的值不会消失。
简单类比如下:
每一个闭包的声明都为Java中class,闭包中用到的属性就是class的属性,创建一个闭包等于实例化一个对象,后续对这个闭包的调用就是对此对象的调用。
代码如下
func main() {
deferOpera1()
}
func deferOpera1() {
var a int
defer func(a *int) { // 这里传递的是指针
println(*a)
}(&a)
a = 12 // 最后a的赋值是可以被defer里面的获取到的,
}
因为传递的是指针,但指针指向的数据是同一个,所以,这里defer可以访问到的。
type number int
func (n number) print() { fmt.Println(n) }
func (n *number) pprint() { fmt.Println(*n) }
func main() {
var n number
defer n.print() // 标号1 结果:0
defer n.pprint() // 标号2 结果: 3
defer func() { n.print() }() //标号3 结果: 3
defer func() { n.pprint() }()// 标号4 结果: 3
n = 3
}
// output:
3
3
3
0
解释
先进后出,所以从标号1到标号4应该倒序输出,为了解释方便,在每个标号中都标注了结果。我们从上往下看
标号1:defer 函数调用,在调用的时候计算结果,非指针,a在这个时候还是默认值0
标号2:指针,会受到影响
标号3:闭包调用,上下文中的变量是可以受到影响的
标号4:闭包调用,指针,受到影响
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()
}
// output:
<nil>
defer error
<nil>
解释
defer解释就到这了。
defer 后面的函数为nil是个什么意思?
func main() {
defer fmt.Println("reachable 1")
var f func() // f is nil by default
defer f() // panic here
// The following lines are also reachable.
fmt.Println("reachable 2")
f = func() {} // useless to avoid panicking
}
defer 有性能损失,但1.13之后就可以忽略不记了。
这里说的单纯的defer的机制,和defer里面的函数没有关系,defer里面的函数的代价和性能和正常的一样
一个非常大的defer队列可能会消耗大量的内存,如果某些调用被延迟得太久,一些资源可能无法及时释放
func writeManyFiles(files []File) error {
for _, file := range files {
f, err := os.Open(file.path)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(file.content)
if err != nil {
return err
}
err = f.Sync()
if err != nil {
return err
}
}
return nil
}
函数处理许多文件时,将会创建大量的文件句柄,这些句柄在函数退出之前可能无法得到释放。
改进方式如下:
上面的问题在于defer队列太长,在函数返回的时候才会一次性执行所有的defer队列,可以利用闭包来提前关闭defer
func writeManyFiles(files []File) error {
for _, file := range files {
// 这里做了闭包,在闭包结束时候,defer就能立马执行,这样defer执行就提前了
if err := func() error {
f, err := os.Open(file.path)
if err != nil {
return err
}
// The close method will be called at
// the end of the current loop step.
defer f.Close()
_, err = f.WriteString(file.content)
if err != nil {
return err
}
return f.Sync()
}(); err != nil {
return err
}
}
return nil
}
可以从panic的goroutine中恢复过来,并且只能在defer中有用,而且还得是匿名函数。
如果正常的放在函数里面当方法调用是没有任何作用的,方法返回nil。
比如,在一些模块中,模块对外输出是error,模块内部的panic,内部recover,对外输出一个error
代码如下:
// 模拟封装模块内部错误,利用defer的规则第三条,函数返回值参数有名字,可以在defer中修改。
func main() {
if err := recover1(); err != nil {
println(err.Error())
}
}
func recover1() (err error) {
defer func() {
if err1 := recover(); err1 != nil {
err = errors.New("inner exception")
fmt.Println("recover success. err: ", err)
}
}()
println("recover1 begin")
recover2()
println("end")
return nil
}
func recover2() {
println("recover2 begin")
panic("error")
println("recover2 end")
}
参考资料
Golang 之轻松化解 defer 的温柔陷阱:https://qcrao.com/post/how-to-keep-off-trap-of-defer/
More about Deferred Function Calls:https://go101.org/article/defer-more.html