Go 语言, defer 实现原理

简介: defer 语句用于延迟函数的调用,使用 defer 关键字修饰一个函数,会将这个函数压入栈中,当函数返回时,再把栈中函数取出执行。
  1. 下面程序输出什么?
func deferTest() {
  var a = 1
  defer fmt.Println(a)
  a = 2
  return
}
复制代码

是:1

解析:延迟函数 fmt.Println(a) 的参数在 defer 语句出现的时候就已经确定下来了,所以不管后面如何修改 a 变量,都不会影响延迟函数。

  1. 下面程序输出什么?
package main
import "fmt"
func main() {
  deferTest()
}
func deferTest() {
  var arr = [3]int{1, 2, 3}
  defer printTest(&arr)
  arr[0] = 4
  return
}
func printTest(array *[3]int) {
  for i := range array {
    fmt.Println(array[i])
  }
}
复制代码

是:

4
2
3
复制代码

解析:延迟函数 printTest() 的参数在 defer 语句出现的时候就已经确定下来了,即为数组的地址,延迟函数执行的时机是在 return 语句之前,所以对数组的最终修改的值会被打印出来。

  1. 下面程序输出什么?
package main
import "fmt"
func main() {
  res := deferTest()
  fmt.Println(res)
}
func deferTest () (result int) {
  i := 1
  defer func() {
    result++
  }()
  return i
}
复制代码

是:

2
复制代码

解析:函数的 return 语句并不是原子级的,实际的执行过程为为设置返回值—>ret,defer 语句是在返回前执行,所以返回过程是:设置返回值—>执行defer—>ret。所以 return 语句先把 result 设置成 i 的值(1),defer 语句中又把 result 递增 1 ,所以最终返回值为 2 。

defer 规则

  • 延迟函数的参数在defer语句出现时就已经确定
  • 注意:对于指针类型的参数,规则仍然适用,不过延迟函数的参数是一个地址值,这种情况下,defer 后面的语句对变量的修改可能会影响延迟函数。
  • 延迟函数执行按 先进后出 顺序执行,即先出现的 defer 最后执行
  • 延迟函数可能操作主函数的具名返回值

函数返回过程

上面题目中我们已经了解到,函数的 return 语句并不是原子级的,实际上 return 语句只代理汇编指令 ret。返回过程是:设置返回值—>执行defer—>ret

func deferTest () (result int) {
  i := 1
  defer func() {
    result++
  }()
  return i
}
复制代码

上面有 defer 例子的return 语句实际执行过程是:

result = i
result++
return
复制代码

主函数拥有匿名返回值,返回字面值时

当主函数有一个匿名返回值,返回时使用字面值,例如返回 “1”,“2”,“3” 这样的值,此时 defer 语句是不能操作返回值的。

func test() int {
  var i int
  defer func() {
    i++
  }()
  return 1
}
复制代码

上面的 return 语句,直接把1作为返回值,延迟函数无法操作返回值,所以也就不能修改返回值。

主函数拥有匿名返回值,返回变量时

当主函数有一个匿名返回值,返回会使用本地或者全局变量,此时 defer 语句可以引用到返回值,但不会改变返回值。

func test() int {
  var i int
  defer func() {
    i++
  }()
  return i
}
复制代码

上面的函数,返回一个局部变量,defer 函数也有操作这个局部变量。对于匿名返回值来说,我们可以假定仍然有一个变量用来存储返回值,例如假定返回值变量为 ”aaa”,上面的返回语句可以拆分成以下过程:

aaa = i
i++
return 
复制代码

由于i是整型,会将值拷贝给变量 aaa,所以defer语句中修改 i的值,对函数返回值不造成影响。

主函数拥有具名返回值时

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

package main
import "fmt"
func main() {
  res := test()
  fmt.Println(res) // 1
}
func test() (i int) {
  defer func() {
    i++
  }()
  return 0
}
复制代码

上面的返回语句可以拆分成以下过程:

i = 0
i++
return 
复制代码

defer实现原理

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

type _defer struct {
  siz     int32 // includes both arguments and results
  started bool
  heap    bool
  // openDefer indicates that this _defer is for a frame with open-coded
  // defers. We have only one defer record for the entire frame (which may
  // currently have 0, 1, or more defers active).
  openDefer bool
  sp        uintptr  // sp at time of defer
  pc        uintptr  // pc at time of defer
  fn        *funcval // can be nil for open-coded defers
  _panic    *_panic  // panic that is running defer
  link      *_defer
  // If openDefer is true, the fields below record values about the stack
  // frame and associated function that has the open-coded defer(s). sp
  // above will be the sp for the frame, and pc will be address of the
  // deferreturn call in the function.
  fd   unsafe.Pointer // funcdata for the function associated with the frame
  varp uintptr        // value of varp for the stack frame
  // framepc is the current pc associated with the stack frame. Together,
  // with sp above (which is the sp associated with the stack frame),
  // framepc/sp can be used as pc/sp pair to continue a stack trace via
  // gentraceback().
  framepc uintptr
}
复制代码
  • sp 函数栈指针
  • pc 程序计数器
  • fn 函数地址
  • link 指向自身结构的指针,用于链接多个 defer

defer 语句后面是要跟一个函数的,所以 defer 的数据结构跟一般的函数类似,不同之处是 defer 结构含有一个指针,用于指向另一个 defer ,每个 goroutine 数据结构中实际上也有一个 defer 指针指向一个 defer 的单链表,每次声明一个defer 时就将 defer 插入单链表的表头,每次执行 defer 时就从单链表的表头取出一个 defer 执行。保证 defer 是按 FIFO 方式执行的。

defer的创建和执行

源码包 src/runtime/panic.go 中定义了两个方法分别用于创建defer和执行defer。

  • deferproc(): 在声明 defer 处调用,其将defer 函数存入 goroutine 的链表中;
  • deferreturn():在 return 指令,准确的讲是在 ret 指令前调用,其将 defer 从 goroutine链表中取出并执行。

归纳总结

  1. defer 定义的延迟函数的参数在 defer 语句出时就已经确定下来了
  2. defer 定义顺序与实际执行顺序相反
  3. return 不是原子级操作的,执行过程是: 保存返回值—>执行 defer —>执行ret
相关文章
|
6天前
|
存储 安全 Java
【Golang】(4)Go里面的指针如何?函数与方法怎么不一样?带你了解Go不同于其他高级语言的语法
结构体可以存储一组不同类型的数据,是一种符合类型。Go抛弃了类与继承,同时也抛弃了构造方法,刻意弱化了面向对象的功能,Go并非是一个传统OOP的语言,但是Go依旧有着OOP的影子,通过结构体和方法也可以模拟出一个类。
53 1
|
2月前
|
Cloud Native 安全 Java
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
238 1
|
8月前
|
编译器 Go
揭秘 Go 语言中空结构体的强大用法
Go 语言中的空结构体 `struct{}` 不包含任何字段,不占用内存空间。它在实际编程中有多种典型用法:1) 结合 map 实现集合(set)类型;2) 与 channel 搭配用于信号通知;3) 申请超大容量的 Slice 和 Array 以节省内存;4) 作为接口实现时明确表示不关注值。此外,需要注意的是,空结构体作为字段时可能会因内存对齐原因占用额外空间。建议将空结构体放在外层结构体的第一个字段以优化内存使用。
|
8月前
|
运维 监控 算法
监控局域网其他电脑:Go 语言迪杰斯特拉算法的高效应用
在信息化时代,监控局域网成为网络管理与安全防护的关键需求。本文探讨了迪杰斯特拉(Dijkstra)算法在监控局域网中的应用,通过计算最短路径优化数据传输和故障检测。文中提供了使用Go语言实现的代码例程,展示了如何高效地进行网络监控,确保局域网的稳定运行和数据安全。迪杰斯特拉算法能减少传输延迟和带宽消耗,及时发现并处理网络故障,适用于复杂网络环境下的管理和维护。
|
2月前
|
Cloud Native Go API
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
278 0
|
2月前
|
Cloud Native Java Go
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
193 0
|
2月前
|
Cloud Native Java 中间件
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
160 0
|
2月前
|
Cloud Native Java Go
Go:为云原生而生的高效语言
Go:为云原生而生的高效语言
243 0
|
2月前
|
数据采集 Go API
Go语言实战案例:多协程并发下载网页内容
本文是《Go语言100个实战案例 · 网络与并发篇》第6篇,讲解如何使用 Goroutine 和 Channel 实现多协程并发抓取网页内容,提升网络请求效率。通过实战掌握高并发编程技巧,构建爬虫、内容聚合器等工具,涵盖 WaitGroup、超时控制、错误处理等核心知识点。
|
2月前
|
数据采集 JSON Go
Go语言实战案例:实现HTTP客户端请求并解析响应
本文是 Go 网络与并发实战系列的第 2 篇,详细介绍如何使用 Go 构建 HTTP 客户端,涵盖请求发送、响应解析、错误处理、Header 与 Body 提取等流程,并通过实战代码演示如何并发请求多个 URL,适合希望掌握 Go 网络编程基础的开发者。

热门文章

最新文章