defer使用场景
先来看一段代码:
func multiplyClose() {
f, err := os.Open("./deferStudy")
if err!=nil {
f.Close()
log.Fatalf("Fail to open file : %v",err)
}
//....
//....
f.Close()
}
无论是否正常退出或者panic退出,我们都需要执行某些特定操作的时候,这个时候defer就起到了至关重要的作用。上面代码中调用了两次Close方法,而目的是一致,即在函数结束时执行一次Close。若代码中仍需要进行多次关键错误判断,多次的使用f.Close()只会造成代码的冗余。这时候我们的主角defer 登场了. ✌️
//延迟对Close这样的函数的调用有两个优点。
//首先,它保证您永远不会忘记关闭文件,如果稍后编辑该函数以添加新的返回路径,则很容易犯这个错误。
//其次,它意味着close位于open附近,这比把它放在函数的末尾要清楚得多。
func multiplyClose() {
f, err := os.Open("./deferStudy")
defer f.Close()
if err!=nil {
//f.Close()
log.Fatalf("Fail to open file : %v",err)
}
//....
//....
}
defer语句经常被⽤于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机制,不论函数逻辑多复杂,都能保证在任何执⾏路径下,资源被释放.
defer机制
defer 后面参数应该是一个函数调用(也可以是方法,是方法的话要包含接收器receiver) 后续我会写一篇关于方法的文章,欢迎支持🥰
func main(){
var i int
defer i=1 //error:Expression in defer must be a function call
}
语义化的去理解它,defer后面应该跟着的是重要的”行为",defer to do Something that has to be done,去做一些我们必须做的。函数在go里可是一等公民💪。不要忘记写小括号!!
defer 的调用顺序采用了“后进先出”的方式,先声明的后调用。
func asStack() {
defer func() {
fmt.Println("3")
}()
defer func() {
fmt.Println("2")
}()
defer func() {
fmt.Println("1")
}()
}
输出:
1
2
3
defer后的函数的参数在定义时就已经决定了。放上一句go官方的解释”The arguments to the deferred function (which include the receiver if the function is a method) are evaluated when the defer executes, not when the call executes."
func evaluated() {
for i := 1; i <= 5; i++ {
defer fmt.Print(i) //若不是在defer executes时期决定,结果将输出55555
}
}
输出:54321
再加一个例子:
func evaluatedArg() {
i := 1
defer func(x int) {
fmt.Printf("in defer:%v\n", x)
fmt.Printf("in defer:%v\n", i)
}(i)
i = 2
defer fmt.Printf("out:%v\n", i)
//i=3 加入改行in defer 中的i会输出为3
}
输出:
out:2
in defer:1
in defer:2
第一个defer传入参数 i,这时 i 的值为1,即 x 已被确定。后续 i 值变为2,第二个defer紧跟着的Printf函数中的 i 在定下来时为2,所以输出2。defer executes时期只能确定参数,不能确定函数内的情况,所以in defer中的i只有在调用时才能确定。为什么会这样呢?🤔
其实defer指令会创建一个叫做_defer的结构体,该结构体内的信息告诉编译器该怎么执行当前defer。
type _defer struct {
siz int32 // 参数和返回值共占用空间大小,这段空间会在_defer结构体后面,
//用于defer注册时候保存参数,并在执行时候拷贝到调用者参数与返回值空间。
started bool // 标记defer是否已经执行
heap bool // 标记该_defer结构体是否分配在堆上
openDefer bool // 标志是否使用open coded defer方式处理defer
sp uintptr // 调用者栈指针,执行时会根据sp判断该defer是否是当前执行调用者注册的
pc uintptr // deferprocStack或deferproc的返回地址
fn *funcval // defer函数,是funcval类型
_panic *_panic // panic链表,用于panic处理
link *_defer // 链接到下一个_defer结构体,即该在_defer之前注册的_defer结构体
fd unsafe.Pointer // funcdata for the function associated with the frame
varp uintptr // value of varp for the stack frame
framepc uintptr
}
defer语句会先申请获取一片空间用于存放_defer结构体本身以及结构体所需内容的空间。如果每次都申请新的_defer,那么消耗的空间自然会非常多,这时缓存机制就起到了很好的作用。_defer会使用之前使用完的_defer的空间,若无旧_defer可用则申请新的_defer (推荐一篇深入理解defer的文章)
我们先看siz字段,其在注册时确定了参数和返回值空间大小(后面我称它为a空间),并将参数保存到a空间中,a空间紧跟在defer结构体之后的,这就是参数在executes defer时期就确定的原因。
接着我们看fn字段 fn指针指向一个funcval类型,funcval结构体内有一个fn字段,指向defer函数入口地址。
使用defer时要注意的点
defer的调用有可能会影响return的返回值
//function 1 提前声明返回值
func returnEff() (i int) {
defer func() {
i++
fmt.Printf("returnEff: %v\n", i)
}()
return
}
func main() {
fmt.Printf("returnResult: %v\n", returnEff())
}
输出:
returnEff: 1
returnResult: 1
//function 2
func returnEff() int {
var i int
defer func() {
i++
fmt.Printf("returnEff: %v\n", i)
}()
return i
}
func main() {
fmt.Printf("returnResult:%v\n", returnEff())
}
输出:
returnEff: 1
returnResult: 0
为什么会这样呢?🤔 其实return并非一个原子操作,分为返回值赋值和RET指令两步。如果是匿名返回值,则在返回时会申请一个匿名变量,返回值会赋给这个匿名变量,函数返回这个匿名变量,所以在匿名返回值的情况下,defer并不会影响到返回值,因为根本就没途径可以获取到匿名变量(函数内根本不知道匿名变量放在哪个地址上)。但是如果是具名返回值,返回值在函数声明时就已经定义了(可以知道返回值地址),返回的变量是能在defer中访问到,而且RET指令执行是在runtime.deferreturn之后的。所以在具名返回值的情况下,defer中对返回值的修改会影响到return的RET操作。
defer应该避免放在loop里
func returnEff() error{
var filenames []string
for _, filename := range filenames {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // Possible resource leak, 'defer' is called in the 'for' loop
}
return nil
}
在IDE中编写代码的时候,其实IDE就已经警告⚠我defer放在了loop中。我们可以设想一下如果filenames中有上万个数据会怎样,defer是在函数结束时执行的,其绑定的是函数而不是代码块。这里就相当于建立了数个open file连接,但连接只有在函数结束时才能close。建立多个连接却不中断,这会导致系统崩溃。
For programmers accustomed to block-level resource management from other languages, defer may seem peculiar, but its most interesting and powerful applications come precisely from the fact that it's not block-based but function-based. In the section on panic and recover we'll see another example of its possibilities。——Go官方
总结一下:
- defer 后面的参数应该是一个函数调用或者方法调用,是方法的话就要带上接收器。
- defer 结构体采用链表的形式连接,从表头插入,从表头取出,表现为“后进先出”,而结构体_defer在申请空间前,会先查看_defer缓冲池有无缓存的_defer,没有可用_defer才会申请新空间。
- defer 函数的参数在注册时期就已经决定了。
- defer 应该避免放在Loop循环中。
参考:
深入go语言之旅 推荐⭐️⭐️⭐️⭐️
go官方的Effective Go 强烈推荐⭐️⭐️⭐️⭐️⭐️
笔者只是一个普通人,文章内容不一定是100%正确的,文章只是我的个人理解。我希望大家看完这篇文章之后,能够去翻阅更多的资料,如果有不同的看法,欢迎指出!😄