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
相关文章
|
3天前
|
SQL 安全 Go
【Go语言专栏】Go语言中的安全审计与漏洞修复
【4月更文挑战第30天】本文介绍了Go语言中的安全审计和漏洞修复实践。安全审计包括代码审查、静态分析、运行时分析、渗透测试和专业服务,借助工具如`go vet`、`staticcheck`、`gosec`等。修复漏洞的方法涉及防止SQL注入、XSS攻击、CSRF、不安全反序列化等。遵循最小权限原则、输入验证等最佳实践,结合持续学习,可提升Go应用安全性。参考[Go安全工作组](https://github.com/golang/security)和[OWASP Top 10](https://owasp.org/www-project-top-ten/)深入学习。
|
1天前
|
存储 缓存 安全
Golang深入浅出之-Go语言中的并发安全容器:sync.Map与sync.Pool
Go语言中的`sync.Map`和`sync.Pool`是并发安全的容器。`sync.Map`提供并发安全的键值对存储,适合快速读取和少写入的情况。注意不要直接遍历Map,应使用`Range`方法。`sync.Pool`是对象池,用于缓存可重用对象,减少内存分配。使用时需注意对象生命周期管理和容量控制。在多goroutine环境下,这两个容器能提高性能和稳定性,但需根据场景谨慎使用,避免不当操作导致的问题。
14 4
|
1天前
|
安全 Go 开发者
Golang深入浅出之-Go语言中的CSP模型:深入理解并发哲学
【5月更文挑战第2天】Go语言的并发编程基于CSP模型,强调通过通信共享内存。核心概念是goroutines(轻量级线程)和channels(用于goroutines间安全数据传输)。常见问题包括数据竞争、死锁和goroutine管理。避免策略包括使用同步原语、复用channel和控制并发。示例展示了如何使用channel和`sync.WaitGroup`避免死锁。理解并发原则和正确应用CSP模型是编写高效安全并发程序的关键。
18 4
|
2天前
|
安全 Go 开发者
Golang深入浅出之-Go语言中的CSP模型:深入理解并发哲学
【5月更文挑战第1天】Go语言基于CSP理论,借助goroutines和channels实现独特的并发模型。Goroutine是轻量级线程,通过`go`关键字启动,而channels提供安全的通信机制。文章讨论了数据竞争、死锁和goroutine泄漏等问题及其避免方法,并提供了一个生产者消费者模型的代码示例。理解CSP和妥善处理并发问题对于编写高效、可靠的Go程序至关重要。
9 2
|
2天前
|
设计模式 Go 调度
Golang深入浅出之-Go语言中的并发模式:Pipeline、Worker Pool等
【5月更文挑战第1天】Go语言并发模拟能力强大,Pipeline和Worker Pool是常用设计模式。Pipeline通过多阶段处理实现高效并行,常见问题包括数据竞争和死锁,可借助通道和`select`避免。Worker Pool控制并发数,防止资源消耗,需注意任务分配不均和goroutine泄露,使用缓冲通道和`sync.WaitGroup`解决。理解和实践这些模式是提升Go并发性能的关键。
15 2
|
2天前
|
JSON 监控 安全
Golang深入浅出之-Go语言中的反射(reflect):原理与实战应用
【5月更文挑战第1天】Go语言的反射允许运行时检查和修改结构,主要通过`reflect`包的`Type`和`Value`实现。然而,滥用反射可能导致代码复杂和性能下降。要安全使用,应注意避免过度使用,始终进行类型检查,并尊重封装。反射的应用包括动态接口实现、JSON序列化和元编程。理解反射原理并谨慎使用是关键,应尽量保持代码静态类型。
11 2
|
2天前
|
Go
Golang深入浅出之-Go语言代码质量与规范:遵循Gofmt与Linting
【5月更文挑战第1天】本文讨论了如何使用`gofmt`和Lint工具提升Go代码质量。`gofmt`负责自动格式化代码,保持风格统一,而Lint工具如`golint`、`govet`、`staticcheck`则进行静态分析,检查潜在错误和未使用的变量。通过集成`gofmt`检查到CI/CD流程,避免格式冲突,并使用Lint工具发现并修复问题,如未处理的错误、不规范命名。遵循这些最佳实践,可提高代码可读性、团队协作效率和可维护性。
9 3
|
3天前
|
JSON 安全 Java
2024年的选择:为什么Go可能是理想的后端语言
【4月更文挑战第27天】Go语言在2024年成为后端开发的热门选择,其简洁设计、内置并发原语和强大工具链备受青睐。文章探讨了Go的设计哲学,如静态类型、垃圾回收和CSP并发模型,并介绍了使用Gin和Echo框架构建Web服务。Go的并发通过goroutines和channels实现,静态类型确保代码稳定性和安全性,快速编译速度利于迭代。Go广泛应用在云计算、微服务等领域,拥有丰富的生态系统和活跃社区,适合作为应对未来技术趋势的语言。
8 0
|
3天前
|
Go 开发者
Golang深入浅出之-Go语言项目构建工具:Makefile与go build
【4月更文挑战第27天】本文探讨了Go语言项目的构建方法,包括`go build`基本命令行工具和更灵活的`Makefile`自动化脚本。`go build`适合简单项目,能直接编译Go源码,但依赖管理可能混乱。通过设置`GOOS`和`GOARCH`可进行跨平台编译。`Makefile`适用于复杂构建流程,能定义多步骤任务,但编写较复杂。在选择构建方式时,应根据项目需求权衡,从`go build`起步,逐渐过渡到Makefile以实现更高效自动化。
14 2
|
3天前
|
存储 Go
Golang深入浅出之-Go语言依赖管理:GOPATH与Go Modules
【4月更文挑战第27天】Go语言依赖管理从`GOPATH`进化到Go Modules。`GOPATH`时代,项目结构混乱,可通过设置多个工作空间管理。Go Modules自Go 1.11起提供更现代的管理方式,通过`go.mod`文件控制依赖。常见问题包括忘记更新`go.mod`、处理本地依赖和模块私有化,可使用`go mod tidy`、`replace`语句和`go mod vendor`解决。理解并掌握Go Modules对现代Go开发至关重要。
12 2