Go —— defer

本文详细解析了Go语言中defer语句的作用、执行时机、行为规则,以及其实现原理,包括数据结构和LIFO执行顺序。重点讨论了defer在函数返回值、资源管理和多函数调用中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

defer

defer 语句用于延迟函数的调用,常用于关闭文件描述符、释放锁等资源释放场景。但 defer 关键字只能作用于函数或函数调用。

defer func(){						// 函数
    fmt.Print("Hello,World!")
}()

defer fmt.Print("Hello,World!")		// 函数调用

1. 执行机制

1.1 执行时机

在Go语言的函数中 return 语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer 语句执行的实际就在返回值操作后,RET指令前。具体如下图所示:

defer执行时机

1.2 行为规则

1)规则一:延迟函数的参数在 defer 语句出现时就已经确定了

示例如下:

func a() {
    i := 0
    defer fmt.Println(i)	// 程序运行打印 0
    i++
    return
}

defer 语句中的 fmt.Println() 参数 i 值 在 defer 出现时就已经确定了,实际上是复制了一份。后面对变量 i 的修改不会影响 fmt.Println() 函数的调用,依旧打印 0。

对于指针类型参数,此规则依然适用,只不过延迟函数的参数是一个地址值,在这种情况下,defer 后面的语句对变量的修改可能会影响延迟函数。

2)规则二:延迟函数按照后进先出 的顺序执行

设计 defer 的初衷是简化函数返回时资源清理的动作,资源往往有依赖顺序,比如申请资源的顺序时 A→B→C,释放的顺序往往又要反向进行。这就是把 defer 设计成 LIFO 的原因

3)规则三:延迟函数可能操作主函数的具名返回值

定义 defer 的函数(下称主函数)可能有返回值,返回值可能有名字(具名返回值),也可能没有返回值(匿名返回值),延迟函数可能会影响返回值。

举个栗子:

func deferFuncReturn() (result int){
    i := 1
    
    defer func() {
        result++
    }()
    return i	// 程序返回 2
}

上面已经介绍过了 defer 的执行时机,该函数的 return 语句可以拆分成下面三行:

	result = i
	result++
	return

主函数有不同的返回方式,包括匿名返回值和具名返回值,但万变不离其宗,只要把 return 语句拆开都可以很好理解,下面分别举例说明:

(1)主函数拥有匿名返回值,返回字面值

一个主函数拥有一个匿名返回值,返回时使用字面值,这种情况下 defer 语句时无法操作返回值的

func foo() int {
	var i int
	
	defer func() {
		i++
	)()

	return 1
}

上面的 return 语句直接把 1 写入栈中作为返回值,延迟函数无法操作该返回值

(2)主函数拥有匿名返回值,返回变量

一个主函数拥有一个匿名返回值,返回本地或全局变量,这种情况下 defer 语句可以引用返回值,反不会改变返回值

func foo() int {
	var i int
	
	defer func(){
		i++
	}
	return i
}

假定返回值变量为 anony ,上面的返回语句可以拆分为以下过程:

	annnoy = i
	i++
	return

函数返回 0

(3)主函数拥有具名返回值

主函数声明语句中带名字的返回值会被初始化为一个局部变量,函数内部可以像使用局部变量一样使用该返回值。如果 defer 语句操作该返回值,则可能改变返回结果。

一个影响函数返回值的例子:

func foo() (ret int) {
	defer func() {
		ret++
	}()
	
	return 0     

上面的函数拆解出来如下所示:

	ret = 0
	ret++
	return

函数真正返回前,在 defer 中对返回值做了 +1 操作,所以函数最终返回 1

2. 实现原理

2.1 数据结构

源码包中 src/src/runtime/runtime2.go:_defer 定义了 defer 的数据结构

type _defer struct {
	...
	sp        uintptr // 函数栈指针
	pc        uintptr // 程序计数器
	fn        func()  // 函数地址
	link      *_defer // 指向自身结构的指针,用于链接多个 defer
	...
}

编译器会把每个延迟函数编译成一个 _defer 实例暂存到 goroutine 数据结构中,待函数结束时再逐个取出执行。
每个defer 语句对应一个 _defer 实例,多个实例使用指针 link 链接起来形成一个单链表,保存到 goroutine 数据结构中。
goroutine 的数据结构如下所示:

type g struct {
	...
	_defer *_defer // defer 链表
	...
}

每次插入 _defer 实例时均插入链表头部,函数执行结束时再依次从头部取出,从而实现后进先出的效果。
一个 goroutine 可能连续调用多个函数,defer 的添加过程跟上述流程一致,进入函数时添加 defer ,离开函数时取出 defer ,所以即便调用多个函数,也总是能保证 defer 是按 LIFO 方式执行的。

3. 小结

  • defer 定义的延迟函数参数在 defer 语句出现时就已经确定了
  • defer 定义的顺序与实际地执行顺序相反
  • return 不是原子操作,执行过程是:保存返回值 → 执行 defer → 执行 ret 跳转
  • 申请资源后立即使用 defer 关闭资源是一个好习惯
### Go语言中 `defer` 的使用场景和工作原理 #### 1. **延迟调用** `defer` 关键字的主要功能是在函数返回前执行某些操作。无论函数是正常退出还是因异常(panic)退出,`defer` 都会被触发[^1]。 #### 2. **资源管理** `defer` 常用于资源的自动释放,比如文件关闭、网络连接断开或锁的解锁等。这有助于避免资源泄漏并简化代码结构。例如: ```go func readFile(filename string) { file, err := os.Open(filename) if err != nil { log.Fatal(err) } defer file.Close() // 确保文件在函数结束时被关闭 // 文件读取逻辑... } ``` 上述代码展示了如何利用 `defer` 自动关闭文件描述符[^5]。 #### 3. **错误处理** 在复杂的业务逻辑中,`defer` 可以用来统一处理错误状态或其他清理任务。例如: ```go func processData(data []byte) (err error) { var conn net.Conn conn, err = net.Dial("tcp", "example.com:80") if err != nil { return err } defer func() { if r := recover(); r != nil { fmt.Println("Recovered from panic:", r) } _ = conn.Close() }() // 数据处理逻辑... return nil } ``` 这里不仅实现了资源释放,还通过匿名函数捕获潜在的 `panic` 并恢复程序运行[^2]。 #### 4. **执行顺序** 当存在多个 `defer` 调用时,它们遵循后进先出(LIFO)原则执行。也就是说,最晚声明的 `defer` 会最早被执行。例如: ```go func example() { defer fmt.Println("First Defer") defer fmt.Println("Second Defer") defer fmt.Println("Third Defer") } // 输出结果为: // Third Defer // Second Defer // First Defer ``` 此行为由设计决定,便于开发者更好地控制清理过程[^4]。 #### 5. **参数值的计算时机** 需要注意的是,`defer` 中传递给目标函数的实际参数会在定义时刻求值而非调用时刻。考虑下面的例子: ```go func main() { i := 5 defer fmt.Println(i) // 此处打印的是当前i的值(5),即便之后修改也不会影响 i++ } ``` 最终输出仍为初始赋值的结果——即 `5`,因为变量绑定发生在创建阶段[^5]。 --- ### 工作原理概述 从实现角度来看,每次遇到 `defer` 表达式时,编译器都会将其记录在一个栈数据结构之中;等到所在函数即将完成之际,则依照逆序逐一弹出这些待办事项加以履行[^3]。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值