golang defer

defer用于在函数返回前执行清理操作,类似于finally。它按照FILO顺序执行,即使有panic,也会执行defer。recover能捕获panic,常与defer结合用于错误处理。Go的defer实现有堆分配、栈分配和开放编码三种方式,1.14版后的开放编码几乎无额外开销。
摘要由CSDN通过智能技术生成

defer 关键字的作用和用法,以及它与 panic 和 recover 的关系

defer 关键字可以用来在函数返回前执行一些清理或释放资源的操作,类似于 Java 和 C# 的 finally 语句块。defer 关键字后面跟着一个函数调用,这个函数调用会被延迟到当前函数返回时执行。如果当前函数发生了 panic 异常,defer 语句也会被执行,并且可以在 defer 函数中使用 recover 函数来捕获和处理 panic 异常。


例如1:

func main() {

    // 声明一个匿名函数并赋值给变量 f

    f := func() {

        fmt.Println("defer begin")

        // recover 函数的正确使用方式

        if err := recover(); err != nil {

            fmt.Println(err)

        }

        fmt.Println("defer end")

    }

    // 在 main 结束时调用上面声明的匿名函数

    defer f()



    fmt.Println("main begin")

    panic("异常信息") // 引发 panic

    fmt.Println("main end")

}



/*

 输出:

 main begin

 defer begin

 异常信息

 defer end

 exit status 2

*/

defer 关键字的三种实现方式:堆分配、栈分配和开放编码,以及它们各自的优缺点和性能影响

堆分配是早期 Go 语言处理 defer 关键字的方法,它会在堆上分配一个 runtime._defer 结构体来存储延迟函数和参数等信息,并将其链接到 Goroutine 的 _defer 链表中。这种方法的缺点是需要额外的内存分配和垃圾回收,以及链表遍历和解引用等操作,导致性能较差。

栈分配是 Go 语言在 1.13 版本引入的一种优化方法,它会在栈上分配一个 runtime._defer 结构体,并将其链接到 Goroutine 的 _defer 链表中。这种方法避免了堆上的内存分配和垃圾回收,但仍然需要链表遍历和解引用等操作。

开放编码是 Go 语言在 1.14 版本引入的另一种优化方法,它会将 defer 关键字后面的函数调用直接内联到当前函数中,并在适当的位置插入必要的代码来保证延迟执行。这种方法消除了 runtime._defer 结构体和 _defer 链表的使用,使得 defer 关键字几乎没有额外开销。

defer 关键字在编译时的转换规则,以及它如何影响函数调用栈帧的结构

defer 关键字在编译时会被转换为对 runtime.deferproc 和 runtime.deferreturn 的调用。runtime.deferproc 负责注册延迟函数,并返回一个布尔值表示是否需要继续执行当前函数;runtime.deferreturn 负责执行延迟函数,并返回一个布尔值表示是否需要恢复 panic 异常。

defer 关键字会影响函数调用栈帧(stack frame)的结构,在栈帧中增加了两个字段:_panic 和 _defer。_panic 字段存储了当前 Goroutine 发生 panic 异常时产生的 runtime._panic 结构体;_defer 字段存储了当前 Goroutine 最新注册的 runtime._defer 结构体。

defer 关键字在运行时的执行过程

defer 关键字在运行时的执行过程分为两个阶段:注册阶段和调用阶段。注册阶段是指 defer 关键字所在的语句被执行时,会调用 runtime.deferproc 函数来创建一个 runtime._defer 结构体,并将其链接到 Goroutine 的 _defer 链表中。调用阶段是指当前函数返回前,会调用 runtime.deferreturn 函数来遍历 _defer 链表,并依次执行每个 runtime._defer 结构体中存储的延迟函数。

defer 关键字会按照先进后出(FILO)的顺序执行延迟函数,也就是说最后注册的延迟函数会最先被执行,最先注册的延迟函数会最后被执行。这样可以保证资源的正确释放顺序,例如关闭文件、解锁互斥量等。

defer 关键字会在注册阶段就对延迟函数和参数进行求值,并将结果存储到 runtime._defer 结构体中。这样可以保证延迟函数和参数在调用阶段不受外部变化的影响,例如变量被重新赋值等。

defer 关键字可以修改当前函数的返回值,在调用阶段通过修改栈帧中存储的返回值来实现。这样可以实现一些特殊的功能,例如捕获错误、记录日志等。

package main

import (
	"fmt"
)

func add(a, b int) (result int) {
	defer func() {
		result += 10 // 在返回之前增加10
	}()
	result = a + b // 计算a和b的和
	return         // 返回result
}

func main() {
	fmt.Println(add(1, 2)) // 打印13
}

defer 关键字可以与 panic 和 recover 配合使用,在调用阶段通过检查 _panic 字段来判断是否发生了 panic 异常,并通过调用 recover 函数来恢复正常状态。这样可以实现一些异常处理和错误恢复的逻辑,例如释放资源、打印堆栈信息等。


package main



import "fmt"



func main() {

    fmt.Println("main begin")

    fmt.Println(f1())

    fmt.Println("main end")

}



func f1() (result int) {

    // 延迟匿名函数

    defer func() {

        // 修改返回值

        result++

        // 捕获 panic 异常

        if err := recover(); err != nil {

            fmt.Println(err)

        }

    }()



    // 延迟普通函数

    defer f2()



    // 引发 panic 异常

    panic("panic error")



    return result

}



func f2() {

    fmt.Println("f2 begin")

    // 修改外部变量(无效)

    result := -1

    fmt.Println("f2 end")

}



/*

 输出:

 main begin

 f2 end

 f2 begin

 panic error

 f2 end

 -1

 main end

*/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值