GO defer详解

GO defer详解

是什么?

关于defer,找到了两篇官方的文章

  • https://go.dev/ref/spec#Defer_statements
  • https://go.dev/blog/defer-panic-and-recover

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执行的伪代码 如下:

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
}

defer的三条规则

defer函数的参数在defer语句执行的时候已经计算了

代码

func a() {
    i := 0
    defer fmt.Println(i) // 0  
    i++
    return
}

defer执行的时候函数的参数已经计算了,保存的是当时变量的值,就是0。

defer语句执行的时候是按照FIFO的顺序执行的
func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}

输出为3210

defer函数可以读取赋值给带有返回值名字参数的函数
func c() (i int) {
    defer func() { i++ }()
    return 1
}
// output:2

c函数的返回值是由返回值变量名的,可以直接读取

函数、指针

函数

defer的使用方式常见的有下面两种

  1. defer 函数不需要操纵外部函数变量

    defer deferOperation1()
    defer func(key string){
        对key做处理
    }(实参)
    

    defer函数里面用到了外部变量,并且通过defer的形参传递了过来。

  2. 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>

解释

  1. f1 非指针,defer执行的时候error还是零值。
  2. f2 闭包, 受到上下文影响。
  3. f3函数传递参数,里面和外面没有关系。

defer解释就到这了。

补充说明

  1. 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
    }
    
  2. defer 有性能损失,但1.13之后就可以忽略不记了。

    这里说的单纯的defer的机制,和defer里面的函数没有关系,defer里面的函数的代价和性能和正常的一样

  3. 一个非常大的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
    }
    

recover

可以从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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值